Skip to content

Commit 681e529

Browse files
committed
Sync Python client with full Go-backend parameter support
1 parent e281b50 commit 681e529

File tree

3 files changed

+282
-138
lines changed

3 files changed

+282
-138
lines changed
Lines changed: 178 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,167 @@
1-
import base64
2-
import urllib.parse
3-
from json import dumps
4-
from typing import Any, Optional, TYPE_CHECKING, Union
1+
from base64 import b64encode
2+
from typing import Any, Dict, Optional, TYPE_CHECKING, Tuple, Union
3+
from urllib.parse import urlencode
4+
from json import dumps as json_dumps
55

6-
from nocasedict import NocaseDict
6+
if TYPE_CHECKING:
7+
from .session import AsyncSession
8+
9+
10+
def _prepare_request_body(
11+
data: Optional[Union[str, bytes, Dict[str, Any]]] = None,
12+
json_data: Optional[Union[Dict, list, str]] = None
13+
) -> Tuple[Optional[Union[str, bytes]], Optional[str]]:
14+
"""
15+
Prepares the request body and determines the appropriate Content-Type.
16+
17+
Priority:
18+
1. If json_data is provided, it takes precedence over data
19+
2. For dict data, uses urlencode
20+
3. Strings/bytes are used as-is
21+
"""
22+
if json_data is not None:
23+
if isinstance(json_data, (dict, list)):
24+
return json_dumps(json_data), 'application/json'
25+
return str(json_data), 'application/json'
26+
27+
if data is not None:
28+
if isinstance(data, dict):
29+
return urlencode(data, doseq=True), 'application/x-www-form-urlencoded'
30+
return data, None
31+
32+
return None, None
33+
34+
35+
def _merge_headers(
36+
session_headers: Optional[Dict[str, str]],
37+
request_headers: Optional[Dict[str, str]],
38+
content_type: Optional[str]
39+
) -> Dict[str, str]:
40+
"""
41+
Merges session headers with request headers, considering Content-Type.
42+
43+
Priority:
44+
1. Headers from current request
45+
2. Headers from session
46+
3. Auto-detected Content-Type
47+
"""
48+
merged = {}
49+
if session_headers:
50+
merged.update(session_headers)
51+
if request_headers:
52+
merged.update(request_headers)
53+
if content_type and 'Content-Type' not in merged:
54+
merged['Content-Type'] = content_type
55+
return merged
756

8-
from async_tls_client.cookies import create_cookie
957

10-
if TYPE_CHECKING:
11-
from async_tls_client.session.session import AsyncSession
58+
def _prepare_cookies(cookies: Optional[Dict[str, str]]) -> Dict[str, str]:
59+
"""Formats cookies into the expected backend format (name=value dict)."""
60+
return cookies or {}
61+
62+
63+
def _prepare_proxy(proxy: Optional[Union[Dict[str, Any], str]]) -> Optional[str]:
64+
"""Formats proxy into connection string."""
65+
if proxy is None:
66+
return None
67+
68+
if isinstance(proxy, str):
69+
return proxy
70+
71+
if isinstance(proxy, dict):
72+
# Proxy with authentication
73+
if 'url' in proxy:
74+
return proxy['url']
75+
76+
scheme = proxy.get('scheme', 'http')
77+
host = proxy.get('host', '')
78+
port = proxy.get('port', '')
79+
username = proxy.get('username')
80+
password = proxy.get('password')
81+
82+
if not host:
83+
return None
84+
85+
# Format with port
86+
host_port = f"{host}:{port}" if port else host
87+
88+
# Add authentication
89+
if username and password:
90+
auth = f"{username}:{password}@"
91+
else:
92+
auth = ""
93+
94+
return f"{scheme}://{auth}{host_port}"
95+
96+
raise TypeError(f"Unsupported proxy type: {type(proxy)}")
1297

1398

1499
def build_payload(
15-
session: "AsyncSession",
16-
method: str,
17-
url: str,
18-
params: Optional[dict[str, Any]] = None,
19-
data: Optional[Union[str, bytes, dict]] = None,
20-
headers: Optional[dict[str, str]] = None,
21-
cookies: Optional[dict[str, str]] = None,
22-
json: Optional[Union[dict, list, str]] = None,
23-
allow_redirects: bool = False,
24-
insecure_skip_verify: bool = False,
25-
timeout_seconds: Optional[int] = None,
26-
proxy: Optional[Union[dict, str]] = None
100+
session: "AsyncSession",
101+
method: str,
102+
url: str,
103+
params: Optional[dict[str, Any]] = None,
104+
data: Optional[Union[str, bytes, dict]] = None,
105+
headers: Optional[dict[str, str]] = None,
106+
cookies: Optional[dict[str, str]] = None,
107+
json: Optional[Union[dict, list, str]] = None,
108+
allow_redirects: bool = False,
109+
insecure_skip_verify: bool = False,
110+
timeout_seconds: Optional[int] = None,
111+
timeout_milliseconds: Optional[int] = None,
112+
proxy: Optional[Union[dict, str]] = None,
113+
request_host_override: Optional[str] = None,
114+
stream_output_path: Optional[str] = None,
115+
stream_output_block_size: Optional[int] = None,
116+
stream_output_eof_symbol: Optional[str] = None
27117
) -> dict:
28-
"""Build payload dictionary for TLS client request."""
29-
# Prepare URL with query parameters
30118
final_url = url
31119
if params:
32-
final_url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
120+
final_url = f"{url}?{urlencode(params, doseq=True)}"
33121

34-
# Prepare request body and content type
35122
request_body, content_type = _prepare_request_body(data, json)
36123

37-
# Merge and clean headers
38124
merged_headers = _merge_headers(session.headers, headers, content_type)
39125

40-
# Prepare cookies
41126
request_cookies = _prepare_cookies(cookies)
42127

43-
# Prepare proxy URL
44128
final_proxy = _prepare_proxy(proxy)
45129

46-
# Build base payload
130+
# Таймаут
131+
timeout_sec = timeout_seconds or session.timeout_seconds
132+
timeout_ms = timeout_milliseconds or session.timeout_milliseconds
133+
134+
if timeout_sec and timeout_ms:
135+
raise ValueError("Cannot specify both timeout_seconds and timeout_milliseconds")
136+
137+
# Транспортные опции
138+
transport_options = {}
139+
if session.idle_conn_timeout is not None:
140+
transport_options["idleConnTimeout"] = int(session.idle_conn_timeout * 1e9)
141+
if session.max_idle_conns is not None:
142+
transport_options["maxIdleConns"] = session.max_idle_conns
143+
if session.max_idle_conns_per_host is not None:
144+
transport_options["maxIdleConnsPerHost"] = session.max_idle_conns_per_host
145+
if session.max_conns_per_host is not None:
146+
transport_options["maxConnsPerHost"] = session.max_conns_per_host
147+
if session.max_response_header_bytes is not None:
148+
transport_options["maxResponseHeaderBytes"] = session.max_response_header_bytes
149+
if session.write_buffer_size is not None:
150+
transport_options["writeBufferSize"] = session.write_buffer_size
151+
if session.read_buffer_size is not None:
152+
transport_options["readBufferSize"] = session.read_buffer_size
153+
if session.disable_keep_alives is not None:
154+
transport_options["disableKeepAlives"] = session.disable_keep_alives
155+
if session.disable_compression is not None:
156+
transport_options["disableCompression"] = session.disable_compression
157+
158+
# Потоковая запись
159+
stream_path = stream_output_path or session.stream_output_path
160+
stream_block = stream_output_block_size or session.stream_output_block_size
161+
stream_eof = stream_output_eof_symbol or session.stream_output_eof_symbol
162+
47163
payload = {
48-
"sessionId": session._session_id,
164+
"sessionId": session.session_id,
49165
"followRedirects": allow_redirects,
50166
"forceHttp1": session.force_http1,
51167
"withDebug": session.debug,
@@ -59,89 +175,44 @@ def build_payload(
59175
"proxyUrl": final_proxy,
60176
"requestUrl": final_url,
61177
"requestMethod": method,
62-
"withoutCookieJar": False,
63-
"withDefaultCookieJar": True,
178+
"withoutCookieJar": session.without_cookie_jar,
179+
"withDefaultCookieJar": session.with_default_cookie_jar,
64180
"requestCookies": request_cookies,
65-
"timeoutSeconds": timeout_seconds or session.timeout_seconds,
181+
"disableIPV4": session.disable_ipv4,
182+
"disableIPV6": session.disable_ipv6,
183+
"isRotatingProxy": session.is_rotating_proxy,
184+
"serverNameOverwrite": session.server_name_overwrite,
185+
"localAddress": session.local_address,
186+
"defaultHeaders": session.default_headers,
187+
"connectHeaders": session.connect_headers,
188+
"streamOutputPath": stream_path,
189+
"streamOutputBlockSize": stream_block,
190+
"streamOutputEOFSymbol": stream_eof,
191+
"requestHostOverride": request_host_override,
192+
"transportOptions": transport_options if transport_options else None
66193
}
67194

68-
# Handle request body encoding
195+
# Таймауты
196+
if timeout_ms:
197+
payload["timeoutMilliseconds"] = timeout_ms
198+
elif timeout_sec:
199+
payload["timeoutSeconds"] = timeout_sec
200+
201+
# Тело запроса
69202
if request_body is not None:
70203
if payload["isByteRequest"]:
71-
payload["requestBody"] = base64.b64encode(request_body).decode()
204+
payload["requestBody"] = b64encode(request_body).decode()
72205
else:
73206
payload["requestBody"] = request_body
74207

75-
# Add certificate pinning if configured
208+
# Сертификаты
76209
if session.certificate_pinning:
77210
payload["certificatePinningHosts"] = session.certificate_pinning
78211

79-
# Configure TLS client parameters
80-
_configure_tls_client(session, payload)
81-
82-
return payload
83-
84-
85-
def _prepare_request_body(data, json):
86-
"""Prepare request body and determine content type."""
87-
if data is None and json is not None:
88-
request_body = json if isinstance(json, (str, bytes)) else dumps(json)
89-
content_type = "application/json"
90-
elif data is not None and not isinstance(data, (str, bytes)):
91-
request_body = urllib.parse.urlencode(data, doseq=True)
92-
content_type = "application/x-www-form-urlencoded"
93-
else:
94-
request_body = data
95-
content_type = None
96-
return request_body, content_type
97-
98-
99-
def _merge_headers(base_headers: NocaseDict, extra_headers: Optional[dict], content_type: Optional[str]) -> NocaseDict:
100-
"""Merge and clean headers."""
101-
merged = NocaseDict(base_headers.copy())
102-
if extra_headers:
103-
merged.update(extra_headers)
104-
# Remove keys with None values
105-
none_keys = [k for k, v in merged.items() if v is None or k is None]
106-
for key in none_keys:
107-
del merged[key]
108-
if content_type and "content-type" not in merged:
109-
merged["Content-Type"] = content_type
110-
return merged
111-
112-
113-
def _prepare_cookies(cookies: Optional[dict[str, str]]) -> list[dict]:
114-
"""Convert cookies dictionary to request format."""
115-
if not cookies:
116-
return []
117-
118-
request_cookies = []
119-
for name, value in cookies.items():
120-
cookie = create_cookie(name, value)
121-
request_cookies.append({
122-
"domain": cookie.domain,
123-
"expires": cookie.expires,
124-
"name": cookie.name,
125-
"path": cookie.path,
126-
"value": cookie.value.replace('"', "")
127-
})
128-
return request_cookies
129-
130-
131-
def _prepare_proxy(proxy: Optional[Union[dict, str]]) -> str:
132-
"""Extract proxy URL from proxy configuration."""
133-
if isinstance(proxy, dict) and "http" in proxy:
134-
return proxy["http"]
135-
if isinstance(proxy, str):
136-
return proxy
137-
return ""
138-
139-
140-
def _configure_tls_client(session: "AsyncSession", payload: dict):
141-
"""Configure TLS client parameters in payload."""
212+
# TLS клиент
142213
if session.client_identifier is None:
143214
payload["tlsClientIdentifier"] = ""
144-
payload["customTlsClient"] = {
215+
custom_client = {
145216
"ja3String": session.ja3_string,
146217
"h2Settings": session.h2_settings,
147218
"h2SettingsOrder": session.h2_settings_order,
@@ -154,9 +225,15 @@ def _configure_tls_client(session: "AsyncSession", payload: dict):
154225
"supportedSignatureAlgorithms": session.supported_signature_algorithms,
155226
"supportedDelegatedCredentialsAlgorithms": session.supported_delegated_credentials_algorithms,
156227
"keyShareCurves": session.key_share_curves,
157-
"alpnProtocols": ["h2", "http/1.1"],
158-
"alpsProtocols": ["h2"],
228+
"alpnProtocols": session.alpn_protocols,
229+
"alpsProtocols": session.alps_protocols,
230+
"echCandidatePayloads": session.ech_candidate_payloads,
231+
"echCandidateCipherSuites": session.ech_candidate_cipher_suites,
232+
"recordSizeLimit": session.record_size_limit
159233
}
234+
payload["customTlsClient"] = {k: v for k, v in custom_client.items() if v is not None}
160235
else:
161236
payload["tlsClientIdentifier"] = session.client_identifier
162237
payload["withRandomTLSExtensionOrder"] = session.random_tls_extension_order
238+
239+
return payload

0 commit comments

Comments
 (0)