Skip to content

Commit 5e9657d

Browse files
committed
Split httpx code from basic data manipulation routines
1 parent 6a9b1e4 commit 5e9657d

File tree

5 files changed

+144
-89
lines changed

5 files changed

+144
-89
lines changed

src/saic_ismart_client_ng/net/client/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from saic_ismart_client_ng.listener import SaicApiListener
88
from saic_ismart_client_ng.model import SaicApiConfiguration
9-
from saic_ismart_client_ng.net.security import decrypt_response, encrypt_request
9+
from saic_ismart_client_ng.net.httpx import decrypt_httpx_response, encrypt_httpx_request
1010

1111

1212
class AbstractSaicClient(ABC):
@@ -24,7 +24,7 @@ def __init__(
2424
self.__client = httpx.AsyncClient(
2525
event_hooks={
2626
"request": [self.invoke_request_listener, self.encrypt_request],
27-
"response": [decrypt_response, self.invoke_response_listener]
27+
"response": [decrypt_httpx_response, self.invoke_response_listener]
2828
}
2929
)
3030

@@ -84,7 +84,7 @@ async def invoke_response_listener(self, response: httpx.Response):
8484
self.__logger.warning(f"Error invoking request listener: {e}", exc_info=e)
8585

8686
async def encrypt_request(self, modified_request: httpx.Request):
87-
return await encrypt_request(
87+
return await encrypt_httpx_request(
8888
modified_request=modified_request,
8989
request_timestamp=datetime.now(),
9090
base_uri=self.configuration.base_uri,
Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22
import hmac
33
import logging
44
from datetime import datetime
5-
from typing import Optional
6-
7-
from httpx import Response, Request
85

96
from saic_ismart_client_ng.crypto_utils import md5_hex_digest, encrypt_aes_cbc_pkcs5_padding, \
107
decrypt_aes_cbc_pkcs5_padding
11-
from saic_ismart_client_ng.net.utils import update_request_with_content, normalize_content_type
8+
from saic_ismart_client_ng.net.utils import normalize_content_type
129

1310
logger = logging.getLogger(__name__)
1411

@@ -50,26 +47,28 @@ def get_app_verification_string(
5047
return ""
5148

5249

53-
async def encrypt_request(
50+
def encrypt_request(
5451
*,
55-
modified_request: Request,
52+
original_request_url: str,
53+
original_request_headers: dict,
54+
original_request_content: str,
5655
request_timestamp: datetime,
5756
base_uri: str,
5857
region: str,
5958
tenant_id: str,
6059
user_token: str = "",
6160
class_name: str = "",
62-
):
63-
original_request_url = modified_request.url
64-
original_content_type = modified_request.headers.get("Content-Type") # 'application/x-www-form-urlencoded'
61+
) -> (str, dict):
62+
original_content_type = original_request_headers.get("Content-Type") # 'application/x-www-form-urlencoded'
6563
if not original_content_type:
6664
modified_content_type = "application/json"
6765
else:
6866
modified_content_type = original_content_type # 'application/x-www-form-urlencoded'
6967
request_content = ""
7068
current_ts = str(int(request_timestamp.timestamp() * 1000))
7169
request_path = str(original_request_url).replace(base_uri, "/")
72-
request_body = modified_request.content.decode("utf-8")
70+
request_body = original_request_content
71+
new_content = original_request_content
7372
if request_body and "multipart" not in original_content_type:
7473
modified_content_type = normalize_content_type(original_content_type)
7574
request_content = request_body.strip()
@@ -84,23 +83,21 @@ async def encrypt_request(
8483
iv_hex = md5_hex_digest(current_ts, False)
8584
if key_hex and iv_hex:
8685
new_content = encrypt_aes_cbc_pkcs5_padding(request_content, key_hex, iv_hex).encode('utf-8')
87-
# Update the request content
88-
update_request_with_content(modified_request, new_content)
8986

90-
modified_request.headers["User-Agent"] = "okhttp/3.14.9"
91-
modified_request.headers["Content-Type"] = f"{modified_content_type};charset=utf-8"
92-
modified_request.headers["Accept"] = "application/json"
93-
modified_request.headers["Accept-Encoding"] = "gzip"
87+
original_request_headers["User-Agent"] = "okhttp/3.14.9"
88+
original_request_headers["Content-Type"] = f"{modified_content_type};charset=utf-8"
89+
original_request_headers["Accept"] = "application/json"
90+
original_request_headers["Accept-Encoding"] = "gzip"
9491

95-
modified_request.headers["REGION"] = region
92+
original_request_headers["REGION"] = region
9693

97-
modified_request.headers["APP-SEND-DATE"] = current_ts
98-
modified_request.headers["APP-CONTENT-ENCRYPTED"] = "1"
99-
modified_request.headers["tenant-id"] = tenant_id
100-
modified_request.headers["User-Type"] = "app"
101-
modified_request.headers["APP-LANGUAGE-TYPE"] = "en"
94+
original_request_headers["APP-SEND-DATE"] = current_ts
95+
original_request_headers["APP-CONTENT-ENCRYPTED"] = "1"
96+
original_request_headers["tenant-id"] = tenant_id
97+
original_request_headers["User-Type"] = "app"
98+
original_request_headers["APP-LANGUAGE-TYPE"] = "en"
10299
if user_token:
103-
modified_request.headers["blade-auth"] = user_token
100+
original_request_headers["blade-auth"] = user_token
104101
app_verification_string = get_app_verification_string(
105102
class_name,
106103
request_path,
@@ -110,20 +107,27 @@ async def encrypt_request(
110107
request_content,
111108
user_token
112109
)
113-
modified_request.headers["ORIGINAL-CONTENT-TYPE"] = modified_content_type
114-
modified_request.headers["APP-VERIFICATION-STRING"] = app_verification_string
110+
original_request_headers["ORIGINAL-CONTENT-TYPE"] = modified_content_type
111+
original_request_headers["APP-VERIFICATION-STRING"] = app_verification_string
112+
return new_content, original_request_headers
115113

116114

117-
async def decrypt_request(req: Request, base_uri: str):
115+
def decrypt_request(
116+
*,
117+
original_request_url: str,
118+
original_request_headers: dict,
119+
original_request_content: str,
120+
base_uri: str,
121+
) -> bytes:
118122
charset = 'utf-8'
119-
req_content = (await req.aread()).decode(charset).strip()
123+
req_content = original_request_content.strip()
120124
if req_content:
121-
app_send_date = req.headers.get("APP-SEND-DATE")
122-
original_content_type = req.headers.get("ORIGINAL-CONTENT-TYPE")
125+
app_send_date = original_request_headers.get("APP-SEND-DATE")
126+
original_content_type = original_request_headers.get("ORIGINAL-CONTENT-TYPE")
123127
if app_send_date and original_content_type:
124-
tenant_id = req.headers['tenant-id']
125-
user_token = req.headers.get('blade-auth', '')
126-
request_path = str(req.url).replace(base_uri, "/")
128+
tenant_id = original_request_headers['tenant-id']
129+
user_token = original_request_headers.get('blade-auth', '')
130+
request_path = original_request_url.replace(base_uri, "/")
127131
key = md5_hex_digest(
128132
md5_hex_digest(
129133
request_path + tenant_id + user_token + "app",
@@ -135,23 +139,26 @@ async def decrypt_request(req: Request, base_uri: str):
135139
decrypted = decrypt_aes_cbc_pkcs5_padding(req_content, key, iv)
136140
if decrypted:
137141
return decrypted.encode(charset)
138-
return req_content
139-
140-
141-
async def decrypt_response(resp: Response):
142-
if resp.is_success:
143-
charset = resp.encoding
144-
resp_content = (await resp.aread()).decode(charset).strip()
145-
if resp_content:
146-
app_send_date = resp.headers.get("APP-SEND-DATE")
147-
original_content_type = resp.headers.get("ORIGINAL-CONTENT-TYPE")
148-
if app_send_date and original_content_type:
149-
original_response_key = app_send_date + "1" + original_content_type
150-
key = md5_hex_digest(original_response_key, False) if len(original_response_key) > 0 else ""
151-
iv = md5_hex_digest(app_send_date, False)
152-
decrypted = decrypt_aes_cbc_pkcs5_padding(resp_content, key, iv)
153-
if decrypted:
154-
resp._content = decrypted.encode(charset)
155-
resp.headers["Content-Length"] = str(len(resp._content))
156-
resp.headers["Content-Type"] = original_content_type
157-
return resp
142+
return original_request_content.encode(charset)
143+
144+
145+
def decrypt_response(
146+
*,
147+
original_response_content: str,
148+
original_response_headers: dict,
149+
original_response_charset: str,
150+
) -> (bytes, dict):
151+
resp_content = original_response_content.strip()
152+
if resp_content:
153+
app_send_date = original_response_headers.get("APP-SEND-DATE")
154+
original_content_type = original_response_headers.get("ORIGINAL-CONTENT-TYPE")
155+
if app_send_date and original_content_type:
156+
original_response_key = app_send_date + "1" + original_content_type
157+
key = md5_hex_digest(original_response_key, False) if len(original_response_key) > 0 else ""
158+
iv = md5_hex_digest(app_send_date, False)
159+
decrypted = decrypt_aes_cbc_pkcs5_padding(resp_content, key, iv)
160+
if decrypted:
161+
resp_content = decrypted
162+
original_response_headers["Content-Type"] = original_content_type
163+
164+
return resp_content.encode(original_response_charset), original_response_headers
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from datetime import datetime
2+
from typing import Union
3+
4+
import httpx
5+
from httpx import Request, Response
6+
from httpx._content import encode_request
7+
8+
from saic_ismart_client_ng.net.crypto import encrypt_request, decrypt_request, decrypt_response
9+
10+
11+
async def encrypt_httpx_request(
12+
*,
13+
modified_request: Request,
14+
request_timestamp: datetime,
15+
base_uri: str,
16+
region: str,
17+
tenant_id: str,
18+
user_token: str = "",
19+
class_name: str = "",
20+
):
21+
new_content, new_headers = encrypt_request(
22+
original_request_url=str(modified_request.url),
23+
original_request_headers=modified_request.headers,
24+
original_request_content=modified_request.content.decode("utf-8"),
25+
request_timestamp=request_timestamp,
26+
base_uri=base_uri,
27+
region=region,
28+
tenant_id=tenant_id,
29+
user_token=user_token,
30+
class_name=class_name
31+
)
32+
update_httpx_request_with_content(modified_request, new_content)
33+
modified_request.headers.update(new_headers)
34+
35+
36+
async def decrypt_httpx_request(req: Request, base_uri: str):
37+
charset = 'utf-8'
38+
req_content = (await req.aread()).decode(charset).strip()
39+
if req_content:
40+
return decrypt_request(
41+
original_request_url=str(req.url),
42+
original_request_headers=req.headers,
43+
original_request_content=req_content,
44+
base_uri=base_uri
45+
)
46+
return req_content
47+
48+
49+
async def decrypt_httpx_response(resp: Response):
50+
if resp.is_success:
51+
charset = resp.encoding
52+
resp_content = (await resp.aread()).decode(charset).strip()
53+
if resp_content:
54+
new_resp_content, new_resp_headers = decrypt_response(
55+
original_response_content=resp_content,
56+
original_response_headers=resp.headers,
57+
original_response_charset=charset
58+
)
59+
update_httpx_request_with_content(resp, new_resp_content)
60+
resp.headers.update(new_resp_headers)
61+
return resp
62+
63+
64+
def update_httpx_request_with_content(modified_request: Union[httpx.Request, httpx.Response], new_content: bytes):
65+
recomputed_headers, recomputed_stream = encode_request(content=new_content)
66+
modified_request.stream = recomputed_stream
67+
modified_request._content = new_content
68+
modified_request.headers.update(recomputed_headers)

src/saic_ismart_client_ng/net/utils.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
import httpx
2-
from httpx._content import encode_request
3-
4-
5-
def update_request_with_content(modified_request: httpx.Request, new_content: bytes):
6-
recomputed_headers, recomputed_stream = encode_request(content=new_content)
7-
modified_request.stream = recomputed_stream
8-
modified_request._content = new_content
9-
modified_request.headers.update(recomputed_headers)
10-
11-
121
def normalize_content_type(original_content_type: str):
132
if 'multipart' in original_content_type:
143
return 'multipart/form-data'

tests/security_test.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import httpx
66
import pytest
77

8-
from saic_ismart_client_ng.net import security
8+
from saic_ismart_client_ng.net.crypto import get_app_verification_string
9+
from saic_ismart_client_ng.net.httpx import encrypt_httpx_request, decrypt_httpx_request
910

1011

1112
def test_get_app_verification_string_valid():
@@ -17,8 +18,8 @@ def test_get_app_verification_string_valid():
1718
request_content = '{"key": "value"}'
1819
user_token = 'dummy_token'
1920

20-
result = security.get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
21-
content_type, request_content, user_token)
21+
result = get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
22+
content_type, request_content, user_token)
2223

2324
assert 'afd4eaf98af2d964f8ea840fc144ee7bae95dbeeeb251d5e3a01371442f92eeb' == result
2425

@@ -38,7 +39,7 @@ async def test_a_request_should_encrypt_properly():
3839
original_request_content = original_request.content.decode('utf-8').strip()
3940
region = 'EU'
4041
tenant_id = '2559'
41-
computed_verification_string = security.get_app_verification_string(
42+
computed_verification_string = get_app_verification_string(
4243
"",
4344
"/with/path?vin=zevin",
4445
str(int(ts.timestamp() * 1000)),
@@ -47,13 +48,8 @@ async def test_a_request_should_encrypt_properly():
4748
original_request_content, ''
4849
)
4950

50-
await security.encrypt_request(
51-
modified_request=original_request,
52-
request_timestamp=ts,
53-
base_uri=base_uri,
54-
region=region,
55-
tenant_id=tenant_id,
56-
)
51+
await encrypt_httpx_request(modified_request=original_request, request_timestamp=ts, base_uri=base_uri,
52+
region=region, tenant_id=tenant_id)
5753
assert original_request != None
5854
assert region == original_request.headers['REGION']
5955
assert tenant_id == original_request.headers['tenant-id']
@@ -78,14 +74,9 @@ async def test_a_request_should_decrypt_properly():
7874
region = 'EU'
7975
tenant_id = '2559'
8076

81-
await security.encrypt_request(
82-
modified_request=original_request,
83-
request_timestamp=ts,
84-
base_uri=base_uri,
85-
region=region,
86-
tenant_id=tenant_id,
87-
)
88-
decrypted = await security.decrypt_request(original_request, base_uri=base_uri)
77+
await encrypt_httpx_request(modified_request=original_request, request_timestamp=ts, base_uri=base_uri,
78+
region=region, tenant_id=tenant_id)
79+
decrypted = await decrypt_httpx_request(original_request, base_uri=base_uri)
8980

9081
assert decrypted != None
9182
decrypted_json = json.loads(decrypted)
@@ -101,8 +92,8 @@ def test_with_empty_request_path():
10192
request_content = '{"key": "value"}'
10293
user_token = 'dummy_token'
10394

104-
result = security.get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
105-
content_type, request_content, user_token)
95+
result = get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
96+
content_type, request_content, user_token)
10697
assert 'ff8cb13ebcce5958e7fbfe602716c653fd72ce78842be87b6d50dccede198735' == result
10798

10899

@@ -115,8 +106,8 @@ def test_with_no_request_content():
115106
request_content = ''
116107
user_token = 'dummy_token'
117108

118-
result = security.get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
119-
content_type, request_content, user_token)
109+
result = get_app_verification_string(clazz_simple_name, request_path, current_ts, tenant_id,
110+
content_type, request_content, user_token)
120111
assert '332c85836aa9afc864282436a740eb2cc778fafd1fea74dd887c1f8de5056de0' == result
121112

122113

0 commit comments

Comments
 (0)