migate 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- migate-1.0.0/LICENSE +21 -0
- migate-1.0.0/PKG-INFO +87 -0
- migate-1.0.0/README.md +49 -0
- migate-1.0.0/pyproject.toml +33 -0
- migate-1.0.0/setup.cfg +4 -0
- migate-1.0.0/src/migate/__init__.py +4 -0
- migate-1.0.0/src/migate/config.py +23 -0
- migate-1.0.0/src/migate/login/captcha.py +69 -0
- migate-1.0.0/src/migate/login/sendcode.py +48 -0
- migate-1.0.0/src/migate/login/verify.py +64 -0
- migate-1.0.0/src/migate/login/verifycode.py +29 -0
- migate-1.0.0/src/migate/passtoken.py +92 -0
- migate-1.0.0/src/migate/service.py +43 -0
- migate-1.0.0/src/migate.egg-info/PKG-INFO +87 -0
- migate-1.0.0/src/migate.egg-info/SOURCES.txt +16 -0
- migate-1.0.0/src/migate.egg-info/dependency_links.txt +1 -0
- migate-1.0.0/src/migate.egg-info/requires.txt +1 -0
- migate-1.0.0/src/migate.egg-info/top_level.txt +1 -0
migate-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 offici5l
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
migate-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: migate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: migate is a simplified Xiaomi authentication gateway for Python projects
|
|
5
|
+
Author: offici5l
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 offici5l
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Repository, https://github.com/offici5l/migate
|
|
28
|
+
Keywords: xiaomi,miunlock,MiUnlockTool,bootloader,unlock,MIUI,HyperOS,Tools,18n_bbs_global,unlockApi,xiaomiio,mi,auth,login,gateway,2fa,Captcha
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.7
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: requests
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# migate
|
|
40
|
+
|
|
41
|
+
**migate** is a simplified Xiaomi authentication gateway for Python projects
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
```bash
|
|
45
|
+
pip install migate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or in pyproject.toml:
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
dependencies = [
|
|
52
|
+
"migate"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import migate
|
|
61
|
+
|
|
62
|
+
service_id = ''
|
|
63
|
+
|
|
64
|
+
service_param = {"sid": service_id}
|
|
65
|
+
|
|
66
|
+
# Required for some service IDs like "unlockApi"
|
|
67
|
+
# service_param["checkSafeAddress"] = True
|
|
68
|
+
|
|
69
|
+
pass_token = migate.get_passtoken(service_param)
|
|
70
|
+
# pass_token returns: {"deviceId", "passToken", "userId"}
|
|
71
|
+
|
|
72
|
+
# ___
|
|
73
|
+
|
|
74
|
+
service = migate.get_service(pass_token, service_id)
|
|
75
|
+
# {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
76
|
+
print(service)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
returns: {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
80
|
+
|
|
81
|
+
___
|
|
82
|
+
|
|
83
|
+
<div align="center">
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
</div>
|
migate-1.0.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# migate
|
|
2
|
+
|
|
3
|
+
**migate** is a simplified Xiaomi authentication gateway for Python projects
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```bash
|
|
7
|
+
pip install migate
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Or in pyproject.toml:
|
|
11
|
+
|
|
12
|
+
```toml
|
|
13
|
+
dependencies = [
|
|
14
|
+
"migate"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import migate
|
|
23
|
+
|
|
24
|
+
service_id = ''
|
|
25
|
+
|
|
26
|
+
service_param = {"sid": service_id}
|
|
27
|
+
|
|
28
|
+
# Required for some service IDs like "unlockApi"
|
|
29
|
+
# service_param["checkSafeAddress"] = True
|
|
30
|
+
|
|
31
|
+
pass_token = migate.get_passtoken(service_param)
|
|
32
|
+
# pass_token returns: {"deviceId", "passToken", "userId"}
|
|
33
|
+
|
|
34
|
+
# ___
|
|
35
|
+
|
|
36
|
+
service = migate.get_service(pass_token, service_id)
|
|
37
|
+
# {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
38
|
+
print(service)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
returns: {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
42
|
+
|
|
43
|
+
___
|
|
44
|
+
|
|
45
|
+
<div align="center">
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "migate"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "migate is a simplified Xiaomi authentication gateway for Python projects"
|
|
5
|
+
authors = [{name = "offici5l"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = {file = "LICENSE"}
|
|
8
|
+
|
|
9
|
+
keywords = ["xiaomi", "miunlock", "MiUnlockTool", "bootloader", "unlock", "MIUI", "HyperOS", "Tools", "18n_bbs_global", "unlockApi", "xiaomiio", "mi", "auth", "login", "gateway", "2fa", "Captcha"]
|
|
10
|
+
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: Apache Software License",
|
|
15
|
+
"Operating System :: OS Independent"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
requires-python = ">=3.7"
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/offici5l/migate"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["setuptools>=61", "wheel"]
|
|
30
|
+
build-backend = "setuptools.build_meta"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
migate-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
agent = "offici5l/migate"
|
|
2
|
+
|
|
3
|
+
HEADERS = {
|
|
4
|
+
"User-Agent": agent,
|
|
5
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
BASE_URL = "https://account.xiaomi.com"
|
|
9
|
+
|
|
10
|
+
SERVICELOGIN_URL = BASE_URL + "/pass/serviceLogin"
|
|
11
|
+
SERVICELOGINAUTH2_URL = SERVICELOGIN_URL + "Auth2"
|
|
12
|
+
|
|
13
|
+
LIST_URL = BASE_URL + "/identity/list"
|
|
14
|
+
|
|
15
|
+
SEND_EM_TICKET = BASE_URL + "/identity/auth/sendEmailTicket"
|
|
16
|
+
|
|
17
|
+
SEND_PH_TICKET = BASE_URL + "/identity/auth/sendPhoneTicket"
|
|
18
|
+
|
|
19
|
+
VERIFY_EM = BASE_URL + "/identity/auth/verifyEmail"
|
|
20
|
+
|
|
21
|
+
VERIFY_PH = BASE_URL + "/identity/auth/verifyPhone"
|
|
22
|
+
|
|
23
|
+
USERQUOTA_URL = BASE_URL + "/identity/pass/sms/userQuota"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import webbrowser
|
|
4
|
+
import requests
|
|
5
|
+
import json
|
|
6
|
+
import http.server
|
|
7
|
+
import socketserver
|
|
8
|
+
import threading
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import platform
|
|
11
|
+
|
|
12
|
+
from migate.config import (
|
|
13
|
+
HEADERS,
|
|
14
|
+
BASE_URL
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
|
18
|
+
def log_message(self, format, *args):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def start_temp_server(directory):
|
|
23
|
+
handler = lambda *args: QuietHandler(*args, directory=directory)
|
|
24
|
+
httpd = socketserver.TCPServer(("127.0.0.1", 0), handler)
|
|
25
|
+
port = httpd.server_address[1]
|
|
26
|
+
|
|
27
|
+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
28
|
+
thread.start()
|
|
29
|
+
|
|
30
|
+
return httpd, port
|
|
31
|
+
|
|
32
|
+
def handle_captcha(send_url, response, cookies, payload, capt_key):
|
|
33
|
+
response_text = json.loads(response.text[11:])
|
|
34
|
+
cap_url = BASE_URL + response_text["captchaUrl"]
|
|
35
|
+
response = requests.get(cap_url, headers=HEADERS)
|
|
36
|
+
cookies.update(response.cookies.get_dict())
|
|
37
|
+
captcha_filename = f"{int(time.time())}_captcha.jpg"
|
|
38
|
+
captcha_dir = Path.home()
|
|
39
|
+
captcha_path = captcha_dir / captcha_filename
|
|
40
|
+
|
|
41
|
+
with open(captcha_path, "wb") as f:
|
|
42
|
+
f.write(response.content)
|
|
43
|
+
|
|
44
|
+
httpd, port = start_temp_server(str(captcha_dir))
|
|
45
|
+
local_url = f"http://127.0.0.1:{port}/{captcha_filename}"
|
|
46
|
+
|
|
47
|
+
if platform.system() == "Linux":
|
|
48
|
+
os.system("xdg-open '" + local_url + "'")
|
|
49
|
+
else:
|
|
50
|
+
webbrowser.open(local_url)
|
|
51
|
+
|
|
52
|
+
print(f"check browser: {local_url}")
|
|
53
|
+
user_input = input(f"Enter Captcha: ")
|
|
54
|
+
payload[capt_key] = user_input
|
|
55
|
+
|
|
56
|
+
httpd.shutdown()
|
|
57
|
+
httpd.server_close()
|
|
58
|
+
|
|
59
|
+
response = requests.post(send_url, headers=HEADERS, data=payload, cookies=cookies)
|
|
60
|
+
|
|
61
|
+
os.remove(captcha_path)
|
|
62
|
+
|
|
63
|
+
response_text = json.loads(response.text[11:])
|
|
64
|
+
|
|
65
|
+
if response_text.get("code") == 87001:
|
|
66
|
+
print("\nIncorrect captcha code\n")
|
|
67
|
+
return handle_captcha(send_url, response, cookies, payload, capt_key)
|
|
68
|
+
|
|
69
|
+
return response
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from migate.login.captcha import handle_captcha
|
|
7
|
+
|
|
8
|
+
from migate.config import (
|
|
9
|
+
HEADERS,
|
|
10
|
+
BASE_URL,
|
|
11
|
+
SEND_EM_TICKET,
|
|
12
|
+
SEND_PH_TICKET,
|
|
13
|
+
USERQUOTA_URL
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def send_verification_code(addressType, cookies):
|
|
18
|
+
|
|
19
|
+
if addressType == "EM":
|
|
20
|
+
send_url = SEND_EM_TICKET
|
|
21
|
+
else:
|
|
22
|
+
send_url = SEND_PH_TICKET
|
|
23
|
+
|
|
24
|
+
payload = {'addressType': addressType, 'contentType': "160040", '_json': "true"}
|
|
25
|
+
response = requests.post(USERQUOTA_URL, data=payload, headers=HEADERS, cookies=cookies)
|
|
26
|
+
|
|
27
|
+
response_text = json.loads(response.text[11:])
|
|
28
|
+
|
|
29
|
+
info = response_text.get('info')
|
|
30
|
+
print(f"Attempts remaining: {info}")
|
|
31
|
+
if info == "0":
|
|
32
|
+
exit(f"Sent too many codes. (to {addressType} )Try again tomorrow")
|
|
33
|
+
|
|
34
|
+
response = requests.post(send_url, headers=HEADERS, cookies=cookies)
|
|
35
|
+
response_text = json.loads(response.text[11:])
|
|
36
|
+
|
|
37
|
+
if response_text.get("code") == 87001:
|
|
38
|
+
print(f'\nCAPTCHA verification required !\n')
|
|
39
|
+
payload = {'icode': "", '_json': "true"}
|
|
40
|
+
response = handle_captcha(send_url, response, cookies, payload, "icode")
|
|
41
|
+
response_text = json.loads(response.text[11:])
|
|
42
|
+
|
|
43
|
+
if response_text.get("code") == 0:
|
|
44
|
+
print(f"\nCode sent to {addressType}")
|
|
45
|
+
else:
|
|
46
|
+
code = response_text.get("code")
|
|
47
|
+
error = response_text.get("tips", response_text) if code == 70022 else response_text
|
|
48
|
+
exit(error)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse, parse_qs
|
|
8
|
+
|
|
9
|
+
from migate.login.sendcode import send_verification_code
|
|
10
|
+
|
|
11
|
+
from migate.login.verifycode import verify_code_ticket
|
|
12
|
+
|
|
13
|
+
from miutility.config import (
|
|
14
|
+
HEADERS,
|
|
15
|
+
LIST_URL,
|
|
16
|
+
SERVICELOGINAUTH2_URL
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_verify(context, auth_data, cookies):
|
|
21
|
+
print(f'\n2FA verification required !\n')
|
|
22
|
+
|
|
23
|
+
params = {
|
|
24
|
+
'sid': auth_data["sid"],
|
|
25
|
+
'supportedMask': "0",
|
|
26
|
+
'context': context
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
response = requests.get(LIST_URL, params=params, headers=HEADERS, cookies=cookies)
|
|
30
|
+
|
|
31
|
+
cookies.update(response.cookies.get_dict())
|
|
32
|
+
|
|
33
|
+
result_json = json.loads(response.text[11:])
|
|
34
|
+
options = result_json.get('options', [])
|
|
35
|
+
|
|
36
|
+
if 8 in options and 4 in options:
|
|
37
|
+
choice = input(f"\nChoose verification method:\n1 = phone\n2 = email\nEnter 1 or 2: ").strip()
|
|
38
|
+
if choice not in ["1", "2"]:
|
|
39
|
+
exit("\nInvalid choice!")
|
|
40
|
+
addressType = "PH" if choice == "1" else "EM"
|
|
41
|
+
elif 4 in options:
|
|
42
|
+
addressType = "PH"
|
|
43
|
+
elif 8 in options:
|
|
44
|
+
addressType = "EM"
|
|
45
|
+
else:
|
|
46
|
+
exit(result_json)
|
|
47
|
+
|
|
48
|
+
send_verification_code(addressType, cookies)
|
|
49
|
+
|
|
50
|
+
url = verify_code_ticket(addressType, cookies)
|
|
51
|
+
|
|
52
|
+
response = requests.get(url, headers=HEADERS, allow_redirects=False, cookies=cookies)
|
|
53
|
+
|
|
54
|
+
url = response.headers.get("Location")
|
|
55
|
+
|
|
56
|
+
response = requests.get(url, headers=HEADERS, allow_redirects=False, cookies=cookies)
|
|
57
|
+
|
|
58
|
+
cookies.update(response.cookies.get_dict())
|
|
59
|
+
|
|
60
|
+
response = requests.post(SERVICELOGINAUTH2_URL, headers=HEADERS, data=auth_data, cookies=cookies)
|
|
61
|
+
|
|
62
|
+
response_text = json.loads(response.text[11:])
|
|
63
|
+
|
|
64
|
+
return response
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from migate.config import (
|
|
5
|
+
HEADERS,
|
|
6
|
+
VERIFY_EM,
|
|
7
|
+
VERIFY_PH
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
def verify_code_ticket(addressType, cookies):
|
|
11
|
+
|
|
12
|
+
url = VERIFY_EM if addressType == "EM" else VERIFY_PH
|
|
13
|
+
|
|
14
|
+
ticket = input(f"Enter code: ").strip()
|
|
15
|
+
response = requests.post(url, data={"ticket": ticket, "trust": "true", '_json': "true"}, headers=HEADERS, cookies=cookies)
|
|
16
|
+
|
|
17
|
+
response_text = json.loads(response.text[11:])
|
|
18
|
+
|
|
19
|
+
if response_text.get("code") == 0:
|
|
20
|
+
url = response_text.get('location')
|
|
21
|
+
return url
|
|
22
|
+
elif response_text.get("code") == 70014:
|
|
23
|
+
#tips = response_text.get("tips")
|
|
24
|
+
print("Invalid code")
|
|
25
|
+
return verify_code_ticket(addressType, cookies)
|
|
26
|
+
else:
|
|
27
|
+
exit(response_text)
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from urllib.parse import urlparse, parse_qs
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import pickle
|
|
9
|
+
|
|
10
|
+
from migate.login.captcha import handle_captcha
|
|
11
|
+
from migate.login.verify import handle_verify
|
|
12
|
+
|
|
13
|
+
from miutility.config import (
|
|
14
|
+
HEADERS,
|
|
15
|
+
SERVICELOGINAUTH2_URL,
|
|
16
|
+
SERVICELOGIN_URL
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def get_passtoken(auth_data):
|
|
20
|
+
|
|
21
|
+
sid = auth_data['sid']
|
|
22
|
+
|
|
23
|
+
cookies_file = Path.home() / f".{sid}" / "cookies.pkl"
|
|
24
|
+
if cookies_file.exists():
|
|
25
|
+
passToken = pickle.load(open(cookies_file, "rb"))
|
|
26
|
+
choice = input(
|
|
27
|
+
f"\nAlready logged in\n"
|
|
28
|
+
f"Account ID: {passToken['userId']}\n\n"
|
|
29
|
+
f"Press 'Enter' to continue\n"
|
|
30
|
+
f"(To log out, type 2 and press Enter.)"
|
|
31
|
+
).strip().lower()
|
|
32
|
+
if choice == "2":
|
|
33
|
+
cookies_file.unlink()
|
|
34
|
+
else:
|
|
35
|
+
return passToken
|
|
36
|
+
|
|
37
|
+
auth_data["_json"] = True
|
|
38
|
+
|
|
39
|
+
response = requests.get(SERVICELOGIN_URL, params=auth_data)
|
|
40
|
+
response_text = json.loads(response.text[11:])
|
|
41
|
+
|
|
42
|
+
auth_data["serviceParam"] = response_text["serviceParam"]
|
|
43
|
+
auth_data["qs"] = response_text["qs"]
|
|
44
|
+
auth_data["callback"] = response_text["callback"]
|
|
45
|
+
auth_data["_sign"] = response_text["_sign"]
|
|
46
|
+
|
|
47
|
+
cookies = {}
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
user = input("Username: ").strip()
|
|
51
|
+
pwd = hashlib.md5(input("Password: ").strip().encode()).hexdigest().upper()
|
|
52
|
+
auth_data["user"] = user
|
|
53
|
+
auth_data["hash"] = pwd
|
|
54
|
+
deviceId = "wb_" + str(uuid.UUID(bytes=hashlib.md5((user + pwd + json.dumps(auth_data, sort_keys=True)).encode()).digest()))
|
|
55
|
+
cookies.update({'deviceId': deviceId})
|
|
56
|
+
response = requests.post(SERVICELOGINAUTH2_URL, headers=HEADERS, data=auth_data, cookies=cookies)
|
|
57
|
+
response_text = json.loads(response.text[11:])
|
|
58
|
+
if response_text.get("code") == 70016:
|
|
59
|
+
print(f"\nInvalid password! or username Please try again.\n")
|
|
60
|
+
continue
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
if response_text.get("code") == 87001:
|
|
64
|
+
print(f'\nCAPTCHA verification required !\n')
|
|
65
|
+
cookies = response.cookies.get_dict()
|
|
66
|
+
response = handle_captcha(SERVICELOGINAUTH2_URL, response, cookies, auth_data, "captCode")
|
|
67
|
+
response_text = json.loads(response.text[11:])
|
|
68
|
+
|
|
69
|
+
if "notificationUrl" in response_text:
|
|
70
|
+
notification_url = response_text["notificationUrl"]
|
|
71
|
+
if any(x in notification_url for x in ["callback", "SetEmail", "BindAppealOrSafePhone"]):
|
|
72
|
+
exit(notification_url)
|
|
73
|
+
context = parse_qs(urlparse(notification_url).query)["context"][0]
|
|
74
|
+
response = handle_verify(context, auth_data, cookies)
|
|
75
|
+
response_text = json.loads(response.text[11:])
|
|
76
|
+
|
|
77
|
+
cookies = response.cookies.get_dict()
|
|
78
|
+
|
|
79
|
+
required = {"deviceId", "passToken", "userId"}
|
|
80
|
+
missing = required - cookies.keys()
|
|
81
|
+
if missing:
|
|
82
|
+
return {"error": f"Missing keys: {', '.join(missing)}"}
|
|
83
|
+
|
|
84
|
+
passToken = {k: cookies[k] for k in required}
|
|
85
|
+
|
|
86
|
+
cookies_file.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
pickle.dump(passToken, open(cookies_file, "wb"))
|
|
88
|
+
|
|
89
|
+
print("\nLogin successful\n")
|
|
90
|
+
return passToken
|
|
91
|
+
|
|
92
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import requests, json, urllib.parse, os, base64, hashlib
|
|
2
|
+
|
|
3
|
+
from urllib.parse import parse_qs, urlparse, quote
|
|
4
|
+
|
|
5
|
+
from migate.config import (
|
|
6
|
+
HEADERS,
|
|
7
|
+
SERVICELOGIN_URL
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
def get_service(cookies, sid):
|
|
11
|
+
response = requests.get(SERVICELOGIN_URL, params={'_json': "true", 'sid': sid}, cookies=cookies, headers=HEADERS)
|
|
12
|
+
|
|
13
|
+
response_text = json.loads(response.text[11:])
|
|
14
|
+
|
|
15
|
+
nonce = response_text.get('nonce')
|
|
16
|
+
ssecurity = response_text.get('ssecurity')
|
|
17
|
+
location = response_text.get('location')
|
|
18
|
+
cUserId = response_text.get('cUserId')
|
|
19
|
+
psecurity = response_text.get('psecurity')
|
|
20
|
+
|
|
21
|
+
sign_text = f"nonce={nonce}&{ssecurity}"
|
|
22
|
+
sha1_digest = hashlib.sha1(sign_text.encode()).digest()
|
|
23
|
+
base64_sign = base64.b64encode(sha1_digest)
|
|
24
|
+
client_sign = quote(base64_sign)
|
|
25
|
+
|
|
26
|
+
url = location + f"&clientSign={client_sign}"
|
|
27
|
+
|
|
28
|
+
response = requests.get(url, headers=HEADERS, cookies=cookies)
|
|
29
|
+
|
|
30
|
+
cookies = response.cookies.get_dict()
|
|
31
|
+
|
|
32
|
+
servicedata = {
|
|
33
|
+
'nonce': nonce,
|
|
34
|
+
'ssecurity': ssecurity,
|
|
35
|
+
'cUserId': cUserId,
|
|
36
|
+
'psecurity': psecurity,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
service = {'servicedata': servicedata}
|
|
40
|
+
|
|
41
|
+
service['cookies'] = cookies
|
|
42
|
+
|
|
43
|
+
return service
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: migate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: migate is a simplified Xiaomi authentication gateway for Python projects
|
|
5
|
+
Author: offici5l
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 offici5l
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Repository, https://github.com/offici5l/migate
|
|
28
|
+
Keywords: xiaomi,miunlock,MiUnlockTool,bootloader,unlock,MIUI,HyperOS,Tools,18n_bbs_global,unlockApi,xiaomiio,mi,auth,login,gateway,2fa,Captcha
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.7
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: requests
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# migate
|
|
40
|
+
|
|
41
|
+
**migate** is a simplified Xiaomi authentication gateway for Python projects
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
```bash
|
|
45
|
+
pip install migate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or in pyproject.toml:
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
dependencies = [
|
|
52
|
+
"migate"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import migate
|
|
61
|
+
|
|
62
|
+
service_id = ''
|
|
63
|
+
|
|
64
|
+
service_param = {"sid": service_id}
|
|
65
|
+
|
|
66
|
+
# Required for some service IDs like "unlockApi"
|
|
67
|
+
# service_param["checkSafeAddress"] = True
|
|
68
|
+
|
|
69
|
+
pass_token = migate.get_passtoken(service_param)
|
|
70
|
+
# pass_token returns: {"deviceId", "passToken", "userId"}
|
|
71
|
+
|
|
72
|
+
# ___
|
|
73
|
+
|
|
74
|
+
service = migate.get_service(pass_token, service_id)
|
|
75
|
+
# {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
76
|
+
print(service)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
returns: {'servicedata': {'nonce', 'ssecurity', 'cUserId', 'psecurity'}, 'cookies': {'serviceToken/popRunToken/new_bbs_serviceToken' ...}}
|
|
80
|
+
|
|
81
|
+
___
|
|
82
|
+
|
|
83
|
+
<div align="center">
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/migate/__init__.py
|
|
5
|
+
src/migate/config.py
|
|
6
|
+
src/migate/passtoken.py
|
|
7
|
+
src/migate/service.py
|
|
8
|
+
src/migate.egg-info/PKG-INFO
|
|
9
|
+
src/migate.egg-info/SOURCES.txt
|
|
10
|
+
src/migate.egg-info/dependency_links.txt
|
|
11
|
+
src/migate.egg-info/requires.txt
|
|
12
|
+
src/migate.egg-info/top_level.txt
|
|
13
|
+
src/migate/login/captcha.py
|
|
14
|
+
src/migate/login/sendcode.py
|
|
15
|
+
src/migate/login/verify.py
|
|
16
|
+
src/migate/login/verifycode.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
migate
|