Skip to content

Commit f9f5496

Browse files
committed
Proxying works!
1 parent ab13390 commit f9f5496

File tree

2 files changed

+178
-70
lines changed

2 files changed

+178
-70
lines changed

main.py

Lines changed: 100 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import httpx
66
from flask import Flask, request, make_response
7-
from saic_ismart_client_ng.net.crypto import decrypt_request, encrypt_response
7+
from saic_ismart_client_ng.net.crypto import decrypt_request, encrypt_response, encrypt_response_full
88
from saic_ismart_client_ng.net.httpx import encrypt_httpx_request, decrypt_httpx_response
99

1010
app = Flask(__name__)
@@ -25,92 +25,122 @@ async def encrypt_httpx_request_wrapper(
2525
)
2626

2727

28+
async def passtrough(path):
29+
passtrough_client = httpx.AsyncClient()
30+
headers = dict(request.headers)
31+
params = request.args
32+
if request.method in ['POST', 'PUT']:
33+
content = request.get_data(parse_form_data=False).decode('utf-8')
34+
else:
35+
content = None
36+
headers.pop('Host')
37+
api_response = await passtrough_client.request(
38+
url=f'{base_uri}{path}',
39+
method=request.method,
40+
params=params,
41+
headers=headers,
42+
data=content,
43+
)
44+
api_response_headers = api_response.headers
45+
api_response_content = api_response.content
46+
api_response_code = api_response.status_code
47+
response = make_response(api_response_content)
48+
response.headers.update(api_response_headers.items())
49+
return response, api_response_code
50+
51+
2852
@app.route('/api.app/v1/', defaults={'path': ''})
2953
@app.route('/api.app/v1/<path:path>', methods=['GET'])
30-
def do_get(path):
31-
print(request)
32-
return 'You want GET path: %s' % path
54+
async def do_get(path):
55+
print(f"do_get {path}")
56+
return await passtrough(path)
3357

3458

3559
@app.route('/api.app/v1/', defaults={'path': ''}, methods=['POST'])
3660
@app.route('/api.app/v1/<path:path>', methods=['POST'])
3761
async def do_post(path):
62+
print(f"do_post {path}")
3863
if path == 'oauth/token':
39-
raw_data = request.get_data(parse_form_data=False).decode('utf-8')
40-
decrypted = decrypt_request(
41-
original_request_url=request.url,
42-
original_request_headers=request.headers,
43-
original_request_content=raw_data,
44-
base_uri=request.url.removesuffix(path),
64+
return await handle_login(path)
65+
else:
66+
return await passtrough(path)
67+
68+
69+
async def handle_login(path):
70+
decrypting_client = httpx.AsyncClient(
71+
event_hooks={
72+
"request": [encrypt_httpx_request_wrapper],
73+
"response": [decrypt_httpx_response]
74+
},
75+
)
76+
raw_data = request.get_data(parse_form_data=False).decode('utf-8')
77+
decrypted = decrypt_request(
78+
original_request_url=request.url,
79+
original_request_headers=request.headers,
80+
original_request_content=raw_data,
81+
base_uri=request.url.removesuffix(path),
82+
)
83+
unquoted = urllib.parse.parse_qs(decrypted)
84+
username = unquoted[b'username'][0].decode('utf-8')
85+
password = unquoted[b'password'][0].decode('utf-8')
86+
device_id = unquoted[b'deviceId'][0].decode('utf-8')
87+
device_type = unquoted[b'deviceType'][0].decode('utf-8')
88+
scope = unquoted[b'scope'][0].decode('utf-8')
89+
grant_type = unquoted[b'grant_type'][0].decode('utf-8')
90+
login_type = unquoted[b'loginType'][0].decode('utf-8')
91+
country_code = unquoted[b'countryCode'][0].decode('utf-8') if b'countryCode' in unquoted else ''
92+
headers = {
93+
"Content-Type": "application/x-www-form-urlencoded",
94+
"Accept": "application/json",
95+
"Authorization": request.headers['Authorization']
96+
}
97+
form_body = {
98+
"grant_type": grant_type,
99+
"username": username,
100+
"password": password,
101+
"scope": scope,
102+
"deviceId": device_id,
103+
"deviceType": device_type,
104+
"loginType": login_type,
105+
"countryCode": country_code
106+
}
107+
api_response = await decrypting_client.post(url=f'{base_uri}{path}', data=form_body, headers=headers)
108+
if api_response.is_success:
109+
cached_tokens[username] = api_response.text
110+
ts = api_response.headers['app-send-date']
111+
new_content, new_headers = encrypt_response_full(
112+
original_request_url=str(api_response.url),
113+
original_response_headers=api_response.headers,
114+
original_response_content=api_response.text,
115+
response_timestamp_ms=ts,
116+
base_uri=base_uri,
117+
tenant_id='459771',
118+
user_token=''
45119
)
46-
unquoted = urllib.parse.parse_qs(decrypted)
47-
username = unquoted[b'username'][0].decode('utf-8')
48-
if username in cached_tokens:
49-
response = cached_tokens[username]
50-
else:
51-
password = unquoted[b'password'][0].decode('utf-8')
52-
login_type = unquoted[b'loginType'][0].decode('utf-8')
53-
country_code = unquoted[b'countryCode'][0].decode('utf-8') if b'countryCode' in unquoted else ''
54-
headers = {
55-
"Content-Type": "application/x-www-form-urlencoded",
56-
"Accept": "application/json",
57-
"Authorization": request.headers['Authorization']
58-
}
59-
firebase_device_id = "cqSHOMG1SmK4k-fzAeK6hr:APA91bGtGihOG5SEQ9hPx3Dtr9o9mQguNiKZrQzboa-1C_UBlRZYdFcMmdfLvh9Q_xA8A0dGFIjkMhZbdIXOYnKfHCeWafAfLXOrxBS3N18T4Slr-x9qpV6FHLMhE9s7I6s89k9lU7DD"
60-
form_body = {
61-
"grant_type": "password",
62-
"username": username,
63-
"password": password,
64-
"scope": "all",
65-
"deviceId": f"{firebase_device_id}###europecar",
66-
"deviceType": "1", # 2 for huawei
67-
"loginType": login_type,
68-
"countryCode": country_code
69-
}
70-
client = httpx.AsyncClient(
71-
event_hooks={
72-
"request": [encrypt_httpx_request_wrapper],
73-
"response": [decrypt_httpx_response]
74-
},
75-
)
76-
response = await client.post(url=f'{base_uri}{path}', data=form_body, headers=headers)
77-
if response.is_success:
78-
response_json = response.json()
79-
cached_tokens[username] = response_json
80-
text_content = json.dumps(response_json)
81-
ts = response.headers['app-send-date']
82-
their_verification = response.headers['app-verification-string']
83-
new_content, new_headers = encrypt_response(
84-
original_request_url=str(response.url),
85-
original_response_headers=response.headers,
86-
original_response_content=response.text,
87-
response_timestamp_ms=ts,
88-
base_uri=base_uri,
89-
tenant_id='459771',
90-
user_token=''
91-
)
92-
response = make_response(new_content)
93-
response.headers.update(new_headers)
94-
return response
95-
else:
96-
return (response.status_code, response.content)
97-
98-
return 'You want POST path: %s' % path
120+
response = make_response(new_content)
121+
generated_headers = dict(new_headers.items())
122+
generated_headers.pop('content-length')
123+
response.headers.update(generated_headers)
124+
return response, api_response.status_code
125+
else:
126+
response = make_response(api_response.text)
127+
response.headers.update(api_response.headers.items())
128+
return response, api_response.status_code
99129

100130

101131
@app.route('/api.app/v1/', defaults={'path': ''}, methods=['DELETE'])
102132
@app.route('/api.app/v1/<path:path>', methods=['DELETE'])
103-
def do_delete(path):
104-
print(request)
105-
return 'You want GET path: %s' % path
133+
async def do_delete(path):
134+
print(f"do_delete {path}")
135+
return await passtrough(path)
106136

107137

108138
@app.route('/api.app/v1/', defaults={'path': ''}, methods=['PUT'])
109139
@app.route('/api.app/v1/<path:path>', methods=['PUT'])
110-
def do_put(path):
111-
print(request)
112-
return 'You want POST path: %s' % path
140+
async def do_put(path):
141+
print(f"do_put {path}")
142+
return await passtrough(path)
113143

114144

115145
if __name__ == '__main__':
116-
app.run()
146+
app.run(host="127.0.0.1", port=8080, debug=True)

pyproject.toml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
[tool.poetry]
2+
name = "saic_python_api_proxy"
3+
homepage = "https://github.com/SAIC-iSmart-API/saic-python-api-proxy"
4+
version = "0.2.3"
5+
description = "MG iSMART API proxy"
6+
authors = [
7+
"Giovanni Condello <[email protected]>",
8+
]
9+
readme = "README.md"
10+
package-mode = false
11+
classifiers = [
12+
"Programming Language :: Python :: 3",
13+
"License :: OSI Approved :: MIT License",
14+
"Operating System :: OS Independent",
15+
]
16+
17+
[tool.poetry.urls]
18+
"Bug Tracker" = "https://github.com/SAIC-iSmart-API/saic-python-api-proxy/issues"
19+
20+
21+
[tool.poetry.dependencies]
22+
python = "^3.11"
23+
httpx = "^0.27.0"
24+
flask = { version = "^3.0.3", extras = ["async"] }
25+
saic_ismart_client_ng = { git = "https://github.com/SAIC-iSmart-API/saic-python-client-ng" }
26+
27+
[tool.poetry.dev-dependencies]
28+
pytest = "^8.2.2"
29+
mock = "^5.1.0"
30+
coverage = "^7.5.4"
31+
ruff = "^0.4.10"
32+
pytest-cov = "^5.0.0"
33+
pytest-asyncio = "^0.23.7"
34+
pytest-mock = "^3.14.0"
35+
36+
37+
38+
[build-system]
39+
requires = ["poetry-core>=1.0.0"]
40+
build-backend = "poetry.core.masonry.api"
41+
42+
[tool.pytest.ini_options]
43+
norecursedirs = ".git build dist"
44+
testpaths = "tests"
45+
#pythonpath = [
46+
# "src"
47+
#]
48+
mock_use_standalone_module = true
49+
addopts = [
50+
"--import-mode=importlib",
51+
]
52+
53+
[tool.coverage.run]
54+
omit = [
55+
"tests/*",
56+
]
57+
branch = true
58+
command_line = "-m pytest"
59+
60+
[tool.coverage.report]
61+
# Regexes for lines to exclude from consideration
62+
exclude_lines = [
63+
# Have to re-enable the standard pragma
64+
'pragma: no cover',
65+
# Don't complain about missing debug-only code:
66+
'def __repr__',
67+
'if self\.debug',
68+
# Don't complain if tests don't hit defensive assertion code:
69+
'raise AssertionError',
70+
'raise NotImplementedError',
71+
# Don't complain if non-runnable code isn't run:
72+
'if 0:',
73+
'if __name__ == .__main__.:',
74+
]
75+
ignore_errors = true
76+
77+
[tool.ruff]
78+
output-format = "github"

0 commit comments

Comments
 (0)