python-tls-client 1.0.2__py3-none-any.whl
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.
- python_tls_client-1.0.2.dist-info/METADATA +187 -0
- python_tls_client-1.0.2.dist-info/RECORD +22 -0
- python_tls_client-1.0.2.dist-info/WHEEL +5 -0
- python_tls_client-1.0.2.dist-info/licenses/LICENSE +22 -0
- python_tls_client-1.0.2.dist-info/top_level.txt +1 -0
- tls_client/__init__.py +15 -0
- tls_client/__version__.py +11 -0
- tls_client/cffi.py +33 -0
- tls_client/cookies.py +456 -0
- tls_client/dependencies/__init__.py +0 -0
- tls_client/dependencies/tls-client-32.dll +0 -0
- tls_client/dependencies/tls-client-64.dll +0 -0
- tls_client/dependencies/tls-client-amd64.so +0 -0
- tls_client/dependencies/tls-client-arm64.dylib +0 -0
- tls_client/dependencies/tls-client-arm64.so +0 -0
- tls_client/dependencies/tls-client-x86.dylib +0 -0
- tls_client/dependencies/tls-client-x86.so +0 -0
- tls_client/exceptions.py +3 -0
- tls_client/response.py +96 -0
- tls_client/sessions.py +518 -0
- tls_client/settings.py +64 -0
- tls_client/structures.py +74 -0
tls_client/sessions.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
from .cffi import request, freeMemory, destroySession
|
|
2
|
+
from .cookies import cookiejar_from_dict, merge_cookies, extract_cookies_to_jar
|
|
3
|
+
from .exceptions import TLSClientExeption
|
|
4
|
+
from .response import build_response, Response
|
|
5
|
+
from .settings import ClientIdentifiers
|
|
6
|
+
from .structures import CaseInsensitiveDict
|
|
7
|
+
from .__version__ import __version__
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
from json import dumps, loads
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import base64
|
|
13
|
+
import ctypes
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Session:
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
client_identifier: ClientIdentifiers = "chrome_120",
|
|
22
|
+
ja3_string: Optional[str] = None,
|
|
23
|
+
h2_settings: Optional[Dict[str, int]] = None,
|
|
24
|
+
h2_settings_order: Optional[List[str]] = None,
|
|
25
|
+
supported_signature_algorithms: Optional[List[str]] = None,
|
|
26
|
+
supported_delegated_credentials_algorithms: Optional[List[str]] = None,
|
|
27
|
+
supported_versions: Optional[List[str]] = None,
|
|
28
|
+
key_share_curves: Optional[List[str]] = None,
|
|
29
|
+
cert_compression_algo: str = None,
|
|
30
|
+
additional_decode: str = None,
|
|
31
|
+
pseudo_header_order: Optional[List[str]] = None,
|
|
32
|
+
connection_flow: Optional[int] = None,
|
|
33
|
+
priority_frames: Optional[list] = None,
|
|
34
|
+
header_order: Optional[List[str]] = None,
|
|
35
|
+
header_priority: Optional[List[str]] = None,
|
|
36
|
+
random_tls_extension_order: Optional = False,
|
|
37
|
+
force_http1: Optional = False,
|
|
38
|
+
catch_panics: Optional = False,
|
|
39
|
+
debug: Optional = False,
|
|
40
|
+
certificate_pinning: Optional[Dict[str, List[str]]] = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._session_id = str(uuid.uuid4())
|
|
43
|
+
# --- Standard Settings ----------------------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
# Case-insensitive dictionary of headers, send on each request
|
|
46
|
+
self.headers = CaseInsensitiveDict(
|
|
47
|
+
{
|
|
48
|
+
"User-Agent": f"tls-client/{__version__}",
|
|
49
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
50
|
+
"Accept": "*/*",
|
|
51
|
+
"Connection": "keep-alive",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Example:
|
|
56
|
+
# {
|
|
57
|
+
# "http": "http://user:pass@ip:port",
|
|
58
|
+
# "https": "http://user:pass@ip:port"
|
|
59
|
+
# }
|
|
60
|
+
self.proxies = {}
|
|
61
|
+
|
|
62
|
+
# Dictionary of querystring data to attach to each request. The dictionary values may be lists for representing
|
|
63
|
+
# multivalued query parameters.
|
|
64
|
+
self.params = {}
|
|
65
|
+
|
|
66
|
+
# CookieJar containing all currently outstanding cookies set on this session
|
|
67
|
+
self.cookies = cookiejar_from_dict({})
|
|
68
|
+
|
|
69
|
+
# Timeout
|
|
70
|
+
self.timeout_seconds = 30
|
|
71
|
+
|
|
72
|
+
# Certificate pinning
|
|
73
|
+
self.certificate_pinning = certificate_pinning
|
|
74
|
+
|
|
75
|
+
# --- Advanced Settings ----------------------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
# Examples:
|
|
78
|
+
# Chrome --> chrome_103, chrome_104, chrome_105, chrome_106
|
|
79
|
+
# Firefox --> firefox_102, firefox_104
|
|
80
|
+
# Opera --> opera_89, opera_90
|
|
81
|
+
# Safari --> safari_15_3, safari_15_6_1, safari_16_0
|
|
82
|
+
# iOS --> safari_ios_15_5, safari_ios_15_6, safari_ios_16_0
|
|
83
|
+
# iPadOS --> safari_ios_15_6
|
|
84
|
+
#
|
|
85
|
+
# for all possible client identifiers, check out the settings.py
|
|
86
|
+
self.client_identifier = client_identifier
|
|
87
|
+
|
|
88
|
+
# Set JA3 --> TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats
|
|
89
|
+
# Example:
|
|
90
|
+
# 771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0
|
|
91
|
+
self.ja3_string = ja3_string
|
|
92
|
+
|
|
93
|
+
# HTTP2 Header Frame Settings
|
|
94
|
+
# Possible Settings:
|
|
95
|
+
# HEADER_TABLE_SIZE
|
|
96
|
+
# SETTINGS_ENABLE_PUSH
|
|
97
|
+
# MAX_CONCURRENT_STREAMS
|
|
98
|
+
# INITIAL_WINDOW_SIZE
|
|
99
|
+
# MAX_FRAME_SIZE
|
|
100
|
+
# MAX_HEADER_LIST_SIZE
|
|
101
|
+
#
|
|
102
|
+
# Example:
|
|
103
|
+
# {
|
|
104
|
+
# "HEADER_TABLE_SIZE": 65536,
|
|
105
|
+
# "MAX_CONCURRENT_STREAMS": 1000,
|
|
106
|
+
# "INITIAL_WINDOW_SIZE": 6291456,
|
|
107
|
+
# "MAX_HEADER_LIST_SIZE": 262144
|
|
108
|
+
# }
|
|
109
|
+
self.h2_settings = h2_settings
|
|
110
|
+
|
|
111
|
+
# HTTP2 Header Frame Settings Order
|
|
112
|
+
# Example:
|
|
113
|
+
# [
|
|
114
|
+
# "HEADER_TABLE_SIZE",
|
|
115
|
+
# "MAX_CONCURRENT_STREAMS",
|
|
116
|
+
# "INITIAL_WINDOW_SIZE",
|
|
117
|
+
# "MAX_HEADER_LIST_SIZE"
|
|
118
|
+
# ]
|
|
119
|
+
self.h2_settings_order = h2_settings_order
|
|
120
|
+
|
|
121
|
+
# Supported Signature Algorithms
|
|
122
|
+
# Possible Settings:
|
|
123
|
+
# PKCS1WithSHA256
|
|
124
|
+
# PKCS1WithSHA384
|
|
125
|
+
# PKCS1WithSHA512
|
|
126
|
+
# PSSWithSHA256
|
|
127
|
+
# PSSWithSHA384
|
|
128
|
+
# PSSWithSHA512
|
|
129
|
+
# ECDSAWithP256AndSHA256
|
|
130
|
+
# ECDSAWithP384AndSHA384
|
|
131
|
+
# ECDSAWithP521AndSHA512
|
|
132
|
+
# PKCS1WithSHA1
|
|
133
|
+
# ECDSAWithSHA1
|
|
134
|
+
#
|
|
135
|
+
# Example:
|
|
136
|
+
# [
|
|
137
|
+
# "ECDSAWithP256AndSHA256",
|
|
138
|
+
# "PSSWithSHA256",
|
|
139
|
+
# "PKCS1WithSHA256",
|
|
140
|
+
# "ECDSAWithP384AndSHA384",
|
|
141
|
+
# "PSSWithSHA384",
|
|
142
|
+
# "PKCS1WithSHA384",
|
|
143
|
+
# "PSSWithSHA512",
|
|
144
|
+
# "PKCS1WithSHA512",
|
|
145
|
+
# ]
|
|
146
|
+
self.supported_signature_algorithms = supported_signature_algorithms
|
|
147
|
+
|
|
148
|
+
# Supported Delegated Credentials Algorithms
|
|
149
|
+
# Possible Settings:
|
|
150
|
+
# PKCS1WithSHA256
|
|
151
|
+
# PKCS1WithSHA384
|
|
152
|
+
# PKCS1WithSHA512
|
|
153
|
+
# PSSWithSHA256
|
|
154
|
+
# PSSWithSHA384
|
|
155
|
+
# PSSWithSHA512
|
|
156
|
+
# ECDSAWithP256AndSHA256
|
|
157
|
+
# ECDSAWithP384AndSHA384
|
|
158
|
+
# ECDSAWithP521AndSHA512
|
|
159
|
+
# PKCS1WithSHA1
|
|
160
|
+
# ECDSAWithSHA1
|
|
161
|
+
#
|
|
162
|
+
# Example:
|
|
163
|
+
# [
|
|
164
|
+
# "ECDSAWithP256AndSHA256",
|
|
165
|
+
# "PSSWithSHA256",
|
|
166
|
+
# "PKCS1WithSHA256",
|
|
167
|
+
# "ECDSAWithP384AndSHA384",
|
|
168
|
+
# "PSSWithSHA384",
|
|
169
|
+
# "PKCS1WithSHA384",
|
|
170
|
+
# "PSSWithSHA512",
|
|
171
|
+
# "PKCS1WithSHA512",
|
|
172
|
+
# ]
|
|
173
|
+
self.supported_delegated_credentials_algorithms = supported_delegated_credentials_algorithms
|
|
174
|
+
|
|
175
|
+
# Supported Versions
|
|
176
|
+
# Possible Settings:
|
|
177
|
+
# GREASE
|
|
178
|
+
# 1.3
|
|
179
|
+
# 1.2
|
|
180
|
+
# 1.1
|
|
181
|
+
# 1.0
|
|
182
|
+
#
|
|
183
|
+
# Example:
|
|
184
|
+
# [
|
|
185
|
+
# "GREASE",
|
|
186
|
+
# "1.3",
|
|
187
|
+
# "1.2"
|
|
188
|
+
# ]
|
|
189
|
+
self.supported_versions = supported_versions
|
|
190
|
+
|
|
191
|
+
# Key Share Curves
|
|
192
|
+
# Possible Settings:
|
|
193
|
+
# GREASE
|
|
194
|
+
# P256
|
|
195
|
+
# P384
|
|
196
|
+
# P521
|
|
197
|
+
# X25519
|
|
198
|
+
#
|
|
199
|
+
# Example:
|
|
200
|
+
# [
|
|
201
|
+
# "GREASE",
|
|
202
|
+
# "X25519"
|
|
203
|
+
# ]
|
|
204
|
+
self.key_share_curves = key_share_curves
|
|
205
|
+
|
|
206
|
+
# Cert Compression Algorithm
|
|
207
|
+
# Examples: "zlib", "brotli", "zstd"
|
|
208
|
+
self.cert_compression_algo = cert_compression_algo
|
|
209
|
+
|
|
210
|
+
# Additional Decode
|
|
211
|
+
# Make sure the go code decodes the response body once explicit by provided algorithm.
|
|
212
|
+
# Examples: null, "gzip", "br", "deflate"
|
|
213
|
+
self.additional_decode = additional_decode
|
|
214
|
+
|
|
215
|
+
# Pseudo Header Order (:authority, :method, :path, :scheme)
|
|
216
|
+
# Example:
|
|
217
|
+
# [
|
|
218
|
+
# ":method",
|
|
219
|
+
# ":authority",
|
|
220
|
+
# ":scheme",
|
|
221
|
+
# ":path"
|
|
222
|
+
# ]
|
|
223
|
+
self.pseudo_header_order = pseudo_header_order
|
|
224
|
+
|
|
225
|
+
# Connection Flow / Window Size Increment
|
|
226
|
+
# Example:
|
|
227
|
+
# 15663105
|
|
228
|
+
self.connection_flow = connection_flow
|
|
229
|
+
|
|
230
|
+
# Example:
|
|
231
|
+
# [
|
|
232
|
+
# {
|
|
233
|
+
# "streamID": 3,
|
|
234
|
+
# "priorityParam": {
|
|
235
|
+
# "weight": 201,
|
|
236
|
+
# "streamDep": 0,
|
|
237
|
+
# "exclusive": false
|
|
238
|
+
# }
|
|
239
|
+
# },
|
|
240
|
+
# {
|
|
241
|
+
# "streamID": 5,
|
|
242
|
+
# "priorityParam": {
|
|
243
|
+
# "weight": 101,
|
|
244
|
+
# "streamDep": false,
|
|
245
|
+
# "exclusive": 0
|
|
246
|
+
# }
|
|
247
|
+
# }
|
|
248
|
+
# ]
|
|
249
|
+
self.priority_frames = priority_frames
|
|
250
|
+
|
|
251
|
+
# Order of your headers
|
|
252
|
+
# Example:
|
|
253
|
+
# [
|
|
254
|
+
# "key1",
|
|
255
|
+
# "key2"
|
|
256
|
+
# ]
|
|
257
|
+
self.header_order = header_order
|
|
258
|
+
|
|
259
|
+
# Header Priority
|
|
260
|
+
# Example:
|
|
261
|
+
# {
|
|
262
|
+
# "streamDep": 1,
|
|
263
|
+
# "exclusive": true,
|
|
264
|
+
# "weight": 1
|
|
265
|
+
# }
|
|
266
|
+
self.header_priority = header_priority
|
|
267
|
+
|
|
268
|
+
# randomize tls extension order
|
|
269
|
+
self.random_tls_extension_order = random_tls_extension_order
|
|
270
|
+
|
|
271
|
+
# force HTTP1
|
|
272
|
+
self.force_http1 = force_http1
|
|
273
|
+
|
|
274
|
+
# catch panics
|
|
275
|
+
# avoid the tls client to print the whole stacktrace when a panic (critical go error) happens
|
|
276
|
+
self.catch_panics = catch_panics
|
|
277
|
+
|
|
278
|
+
# debugging
|
|
279
|
+
self.debug = debug
|
|
280
|
+
|
|
281
|
+
def __enter__(self):
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def __exit__(self, *args):
|
|
285
|
+
self.close()
|
|
286
|
+
|
|
287
|
+
def close(self) -> str:
|
|
288
|
+
destroy_session_payload = {
|
|
289
|
+
"sessionId": self._session_id
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
destroy_session_response = destroySession(dumps(destroy_session_payload).encode('utf-8'))
|
|
293
|
+
# we dereference the pointer to a byte array
|
|
294
|
+
destroy_session_response_bytes = ctypes.string_at(destroy_session_response)
|
|
295
|
+
# convert our byte array to a string (tls client returns json)
|
|
296
|
+
destroy_session_response_string = destroy_session_response_bytes.decode('utf-8')
|
|
297
|
+
# convert response string to json
|
|
298
|
+
destroy_session_response_object = loads(destroy_session_response_string)
|
|
299
|
+
|
|
300
|
+
freeMemory(destroy_session_response_object['id'].encode('utf-8'))
|
|
301
|
+
|
|
302
|
+
return destroy_session_response_string
|
|
303
|
+
|
|
304
|
+
def execute_request(
|
|
305
|
+
self,
|
|
306
|
+
method: str,
|
|
307
|
+
url: str,
|
|
308
|
+
params: Optional[dict] = None, # Optional[dict[str, str]]
|
|
309
|
+
data: Optional[Union[str, dict]] = None,
|
|
310
|
+
headers: Optional[dict] = None, # Optional[dict[str, str]]
|
|
311
|
+
cookies: Optional[dict] = None, # Optional[dict[str, str]]
|
|
312
|
+
json: Optional[dict] = None, # Optional[dict]
|
|
313
|
+
allow_redirects: Optional[bool] = False,
|
|
314
|
+
insecure_skip_verify: Optional[bool] = False,
|
|
315
|
+
timeout_seconds: Optional[int] = None,
|
|
316
|
+
proxy: Optional[dict] = None # Optional[dict[str, str]]
|
|
317
|
+
) -> Response:
|
|
318
|
+
# --- URL ------------------------------------------------------------------------------------------------------
|
|
319
|
+
# Prepare URL - add params to url
|
|
320
|
+
if params is not None:
|
|
321
|
+
url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
|
|
322
|
+
|
|
323
|
+
# --- Request Body ---------------------------------------------------------------------------------------------
|
|
324
|
+
# Prepare request body - build request body
|
|
325
|
+
# Data has priority. JSON is only used if data is None.
|
|
326
|
+
if data is None and json is not None:
|
|
327
|
+
if type(json) in [dict, list]:
|
|
328
|
+
json = dumps(json)
|
|
329
|
+
request_body = json
|
|
330
|
+
content_type = "application/json"
|
|
331
|
+
elif data is not None and type(data) not in [str, bytes]:
|
|
332
|
+
request_body = urllib.parse.urlencode(data, doseq=True)
|
|
333
|
+
content_type = "application/x-www-form-urlencoded"
|
|
334
|
+
else:
|
|
335
|
+
request_body = data
|
|
336
|
+
content_type = None
|
|
337
|
+
# set content type if it isn't set
|
|
338
|
+
if content_type is not None and "content-type" not in self.headers:
|
|
339
|
+
self.headers["Content-Type"] = content_type
|
|
340
|
+
|
|
341
|
+
# --- Headers --------------------------------------------------------------------------------------------------
|
|
342
|
+
if self.headers is None:
|
|
343
|
+
headers = CaseInsensitiveDict(headers)
|
|
344
|
+
elif headers is None:
|
|
345
|
+
headers = self.headers
|
|
346
|
+
else:
|
|
347
|
+
merged_headers = CaseInsensitiveDict(self.headers)
|
|
348
|
+
merged_headers.update(headers)
|
|
349
|
+
|
|
350
|
+
# Remove items, where the key or value is set to None.
|
|
351
|
+
none_keys = [k for (k, v) in merged_headers.items() if v is None or k is None]
|
|
352
|
+
for key in none_keys:
|
|
353
|
+
del merged_headers[key]
|
|
354
|
+
|
|
355
|
+
headers = merged_headers
|
|
356
|
+
|
|
357
|
+
# --- Cookies --------------------------------------------------------------------------------------------------
|
|
358
|
+
cookies = cookies or {}
|
|
359
|
+
# Merge with session cookies
|
|
360
|
+
cookies = merge_cookies(self.cookies, cookies)
|
|
361
|
+
# turn cookie jar into dict
|
|
362
|
+
# in the cookie value the " gets removed, because the fhttp library in golang doesn't accept the character
|
|
363
|
+
request_cookies = [
|
|
364
|
+
{'domain': c.domain, 'expires': c.expires, 'name': c.name, 'path': c.path, 'value': c.value.replace('"', "")}
|
|
365
|
+
for c in cookies
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
# --- Proxy ----------------------------------------------------------------------------------------------------
|
|
369
|
+
proxy = proxy or self.proxies
|
|
370
|
+
|
|
371
|
+
if type(proxy) is dict and "http" in proxy:
|
|
372
|
+
proxy = proxy["http"]
|
|
373
|
+
elif type(proxy) is str:
|
|
374
|
+
proxy = proxy
|
|
375
|
+
else:
|
|
376
|
+
proxy = ""
|
|
377
|
+
|
|
378
|
+
# --- Timeout --------------------------------------------------------------------------------------------------
|
|
379
|
+
# maximum time to wait for a response
|
|
380
|
+
|
|
381
|
+
timeout_seconds = timeout_seconds or self.timeout_seconds
|
|
382
|
+
|
|
383
|
+
# --- Certificate pinning --------------------------------------------------------------------------------------
|
|
384
|
+
# pins a certificate so that it restricts which certificates are considered valid
|
|
385
|
+
|
|
386
|
+
certificate_pinning = self.certificate_pinning
|
|
387
|
+
|
|
388
|
+
# --- Request --------------------------------------------------------------------------------------------------
|
|
389
|
+
is_byte_request = isinstance(request_body, (bytes, bytearray))
|
|
390
|
+
request_payload = {
|
|
391
|
+
"sessionId": self._session_id,
|
|
392
|
+
"followRedirects": allow_redirects,
|
|
393
|
+
"forceHttp1": self.force_http1,
|
|
394
|
+
"withDebug": self.debug,
|
|
395
|
+
"catchPanics": self.catch_panics,
|
|
396
|
+
"headers": dict(headers),
|
|
397
|
+
"headerOrder": self.header_order,
|
|
398
|
+
"insecureSkipVerify": insecure_skip_verify,
|
|
399
|
+
"isByteRequest": is_byte_request,
|
|
400
|
+
# Force the Go backend to return the raw response body as a base64
|
|
401
|
+
# data-URI. Without this flag, binary bytes > 127 get mangled by
|
|
402
|
+
# UTF-8 transcoding (byteReplacer U+FFFD) inside the C library,
|
|
403
|
+
# corrupting binary responses (protobuf, images, ...).
|
|
404
|
+
"isByteResponse": True,
|
|
405
|
+
"additionalDecode": self.additional_decode,
|
|
406
|
+
"proxyUrl": proxy,
|
|
407
|
+
"requestUrl": url,
|
|
408
|
+
"requestMethod": method,
|
|
409
|
+
"requestBody": base64.b64encode(request_body).decode() if is_byte_request else request_body,
|
|
410
|
+
"requestCookies": request_cookies,
|
|
411
|
+
"timeoutSeconds": timeout_seconds,
|
|
412
|
+
}
|
|
413
|
+
if certificate_pinning:
|
|
414
|
+
request_payload["certificatePinningHosts"] = certificate_pinning
|
|
415
|
+
if self.client_identifier is None:
|
|
416
|
+
request_payload["customTlsClient"] = {
|
|
417
|
+
"ja3String": self.ja3_string,
|
|
418
|
+
"h2Settings": self.h2_settings,
|
|
419
|
+
"h2SettingsOrder": self.h2_settings_order,
|
|
420
|
+
"pseudoHeaderOrder": self.pseudo_header_order,
|
|
421
|
+
"connectionFlow": self.connection_flow,
|
|
422
|
+
"priorityFrames": self.priority_frames,
|
|
423
|
+
"headerPriority": self.header_priority,
|
|
424
|
+
"certCompressionAlgo": self.cert_compression_algo,
|
|
425
|
+
"supportedVersions": self.supported_versions,
|
|
426
|
+
"supportedSignatureAlgorithms": self.supported_signature_algorithms,
|
|
427
|
+
"supportedDelegatedCredentialsAlgorithms": self.supported_delegated_credentials_algorithms ,
|
|
428
|
+
"keyShareCurves": self.key_share_curves,
|
|
429
|
+
}
|
|
430
|
+
else:
|
|
431
|
+
request_payload["tlsClientIdentifier"] = self.client_identifier
|
|
432
|
+
request_payload["withRandomTLSExtensionOrder"] = self.random_tls_extension_order
|
|
433
|
+
|
|
434
|
+
# this is a pointer to the response
|
|
435
|
+
response = request(dumps(request_payload).encode('utf-8'))
|
|
436
|
+
# dereference the pointer to a byte array
|
|
437
|
+
response_bytes = ctypes.string_at(response)
|
|
438
|
+
# convert our byte array to a string (tls client returns json)
|
|
439
|
+
response_string = response_bytes.decode('utf-8')
|
|
440
|
+
# convert response string to json
|
|
441
|
+
response_object = loads(response_string)
|
|
442
|
+
# free the memory
|
|
443
|
+
freeMemory(response_object['id'].encode('utf-8'))
|
|
444
|
+
# --- Response -------------------------------------------------------------------------------------------------
|
|
445
|
+
# Error handling
|
|
446
|
+
if response_object["status"] == 0:
|
|
447
|
+
raise TLSClientExeption(response_object["body"])
|
|
448
|
+
# Set response cookies
|
|
449
|
+
response_cookie_jar = extract_cookies_to_jar(
|
|
450
|
+
request_url=url,
|
|
451
|
+
request_headers=headers,
|
|
452
|
+
cookie_jar=cookies,
|
|
453
|
+
response_headers=response_object["headers"]
|
|
454
|
+
)
|
|
455
|
+
# build response class
|
|
456
|
+
return build_response(response_object, response_cookie_jar)
|
|
457
|
+
|
|
458
|
+
def get(
|
|
459
|
+
self,
|
|
460
|
+
url: str,
|
|
461
|
+
**kwargs: Any
|
|
462
|
+
) -> Response:
|
|
463
|
+
"""Sends a GET request"""
|
|
464
|
+
return self.execute_request(method="GET", url=url, **kwargs)
|
|
465
|
+
|
|
466
|
+
def options(
|
|
467
|
+
self,
|
|
468
|
+
url: str,
|
|
469
|
+
**kwargs: Any
|
|
470
|
+
) -> Response:
|
|
471
|
+
"""Sends a OPTIONS request"""
|
|
472
|
+
return self.execute_request(method="OPTIONS", url=url, **kwargs)
|
|
473
|
+
|
|
474
|
+
def head(
|
|
475
|
+
self,
|
|
476
|
+
url: str,
|
|
477
|
+
**kwargs: Any
|
|
478
|
+
) -> Response:
|
|
479
|
+
"""Sends a HEAD request"""
|
|
480
|
+
return self.execute_request(method="HEAD", url=url, **kwargs)
|
|
481
|
+
|
|
482
|
+
def post(
|
|
483
|
+
self,
|
|
484
|
+
url: str,
|
|
485
|
+
data: Optional[Union[str, dict]] = None,
|
|
486
|
+
json: Optional[dict] = None,
|
|
487
|
+
**kwargs: Any
|
|
488
|
+
) -> Response:
|
|
489
|
+
"""Sends a POST request"""
|
|
490
|
+
return self.execute_request(method="POST", url=url, data=data, json=json, **kwargs)
|
|
491
|
+
|
|
492
|
+
def put(
|
|
493
|
+
self,
|
|
494
|
+
url: str,
|
|
495
|
+
data: Optional[Union[str, dict]] = None,
|
|
496
|
+
json: Optional[dict] = None,
|
|
497
|
+
**kwargs: Any
|
|
498
|
+
) -> Response:
|
|
499
|
+
"""Sends a PUT request"""
|
|
500
|
+
return self.execute_request(method="PUT", url=url, data=data, json=json, **kwargs)
|
|
501
|
+
|
|
502
|
+
def patch(
|
|
503
|
+
self,
|
|
504
|
+
url: str,
|
|
505
|
+
data: Optional[Union[str, dict]] = None,
|
|
506
|
+
json: Optional[dict] = None,
|
|
507
|
+
**kwargs: Any
|
|
508
|
+
) -> Response:
|
|
509
|
+
"""Sends a PATCH request"""
|
|
510
|
+
return self.execute_request(method="PATCH", url=url, data=data, json=json, **kwargs)
|
|
511
|
+
|
|
512
|
+
def delete(
|
|
513
|
+
self,
|
|
514
|
+
url: str,
|
|
515
|
+
**kwargs: Any
|
|
516
|
+
) -> Response:
|
|
517
|
+
"""Sends a DELETE request"""
|
|
518
|
+
return self.execute_request(method="DELETE", url=url, **kwargs)
|
tls_client/settings.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing_extensions import Literal, TypeAlias
|
|
2
|
+
|
|
3
|
+
ClientIdentifiers: TypeAlias = Literal[
|
|
4
|
+
# Chrome
|
|
5
|
+
"chrome_103",
|
|
6
|
+
"chrome_104",
|
|
7
|
+
"chrome_105",
|
|
8
|
+
"chrome_106",
|
|
9
|
+
"chrome_107",
|
|
10
|
+
"chrome_108",
|
|
11
|
+
"chrome_109",
|
|
12
|
+
"chrome_110",
|
|
13
|
+
"chrome_111",
|
|
14
|
+
"chrome_112",
|
|
15
|
+
"chrome_116_PSK",
|
|
16
|
+
"chrome_116_PSK_PQ",
|
|
17
|
+
"chrome_117",
|
|
18
|
+
"chrome_120",
|
|
19
|
+
# Safari
|
|
20
|
+
"safari_15_6_1",
|
|
21
|
+
"safari_16_0",
|
|
22
|
+
# iOS (Safari)
|
|
23
|
+
"safari_ios_15_5",
|
|
24
|
+
"safari_ios_15_6",
|
|
25
|
+
"safari_ios_16_0",
|
|
26
|
+
# iPadOS (Safari)
|
|
27
|
+
"safari_ios_15_6",
|
|
28
|
+
# FireFox
|
|
29
|
+
"firefox_102",
|
|
30
|
+
"firefox_104",
|
|
31
|
+
"firefox_105",
|
|
32
|
+
"firefox_106",
|
|
33
|
+
"firefox_108",
|
|
34
|
+
"firefox_110",
|
|
35
|
+
"firefox_117",
|
|
36
|
+
"firefox_120",
|
|
37
|
+
# Opera
|
|
38
|
+
"opera_89",
|
|
39
|
+
"opera_90",
|
|
40
|
+
"opera_91",
|
|
41
|
+
# OkHttp4
|
|
42
|
+
"okhttp4_android_7",
|
|
43
|
+
"okhttp4_android_8",
|
|
44
|
+
"okhttp4_android_9",
|
|
45
|
+
"okhttp4_android_10",
|
|
46
|
+
"okhttp4_android_11",
|
|
47
|
+
"okhttp4_android_12",
|
|
48
|
+
"okhttp4_android_13",
|
|
49
|
+
# Custom
|
|
50
|
+
"zalando_ios_mobile",
|
|
51
|
+
"zalando_android_mobile",
|
|
52
|
+
"nike_ios_mobile",
|
|
53
|
+
"nike_android_mobile",
|
|
54
|
+
"mms_ios",
|
|
55
|
+
"mms_ios_2",
|
|
56
|
+
"mms_ios_3",
|
|
57
|
+
"mesh_ios",
|
|
58
|
+
"mesh_ios_2",
|
|
59
|
+
"mesh_android",
|
|
60
|
+
"mesh_android_2",
|
|
61
|
+
"confirmed_ios",
|
|
62
|
+
"confirmed_android",
|
|
63
|
+
"confirmed_android_2",
|
|
64
|
+
]
|
tls_client/structures.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import MutableMapping, Mapping
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CaseInsensitiveDict(MutableMapping):
|
|
6
|
+
"""Origin: requests library (https://github.com/psf/requests)
|
|
7
|
+
|
|
8
|
+
A case-insensitive ``dict``-like object.
|
|
9
|
+
|
|
10
|
+
Implements all methods and operations of
|
|
11
|
+
``MutableMapping`` as well as dict's ``copy``. Also
|
|
12
|
+
provides ``lower_items``.
|
|
13
|
+
|
|
14
|
+
All keys are expected to be strings. The structure remembers the
|
|
15
|
+
case of the last key to be set, and ``iter(instance)``,
|
|
16
|
+
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
|
17
|
+
will contain case-sensitive keys. However, querying and contains
|
|
18
|
+
testing is case insensitive::
|
|
19
|
+
|
|
20
|
+
cid = CaseInsensitiveDict()
|
|
21
|
+
cid['Accept'] = 'application/json'
|
|
22
|
+
cid['aCCEPT'] == 'application/json' # True
|
|
23
|
+
list(cid) == ['Accept'] # True
|
|
24
|
+
|
|
25
|
+
For example, ``headers['content-encoding']`` will return the
|
|
26
|
+
value of a ``'Content-Encoding'`` response header, regardless
|
|
27
|
+
of how the header name was originally stored.
|
|
28
|
+
|
|
29
|
+
If the constructor, ``.update``, or equality comparison
|
|
30
|
+
operations are given keys that have equal ``.lower()``s, the
|
|
31
|
+
behavior is undefined.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, data=None, **kwargs):
|
|
35
|
+
self._store = OrderedDict()
|
|
36
|
+
if data is None:
|
|
37
|
+
data = {}
|
|
38
|
+
self.update(data, **kwargs)
|
|
39
|
+
|
|
40
|
+
def __setitem__(self, key, value):
|
|
41
|
+
# Use the lowercased key for lookups, but store the actual
|
|
42
|
+
# key alongside the value.
|
|
43
|
+
self._store[key.lower()] = (key, value)
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, key):
|
|
46
|
+
return self._store[key.lower()][1]
|
|
47
|
+
|
|
48
|
+
def __delitem__(self, key):
|
|
49
|
+
del self._store[key.lower()]
|
|
50
|
+
|
|
51
|
+
def __iter__(self):
|
|
52
|
+
return (casedkey for casedkey, mappedvalue in self._store.values())
|
|
53
|
+
|
|
54
|
+
def __len__(self):
|
|
55
|
+
return len(self._store)
|
|
56
|
+
|
|
57
|
+
def lower_items(self):
|
|
58
|
+
"""Like iteritems(), but with all lowercase keys."""
|
|
59
|
+
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
|
|
60
|
+
|
|
61
|
+
def __eq__(self, other):
|
|
62
|
+
if isinstance(other, Mapping):
|
|
63
|
+
other = CaseInsensitiveDict(other)
|
|
64
|
+
else:
|
|
65
|
+
return NotImplemented
|
|
66
|
+
# Compare insensitively
|
|
67
|
+
return dict(self.lower_items()) == dict(other.lower_items())
|
|
68
|
+
|
|
69
|
+
# Copy is required
|
|
70
|
+
def copy(self):
|
|
71
|
+
return CaseInsensitiveDict(self._store.values())
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
return str(dict(self.items()))
|