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
1499def 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