Spaces:
Runtime error
Runtime error
from .cffi import request | |
from .cookies import cookiejar_from_dict, get_cookie_header, merge_cookies, extract_cookies_to_jar | |
from .exceptions import TLSClientExeption | |
from .response import build_response | |
from .structures import CaseInsensitiveDict | |
from .__version__ import __version__ | |
from typing import Any, Optional, Union | |
from json import dumps, loads | |
import urllib.parse | |
import base64 | |
import ctypes | |
import uuid | |
class Session: | |
def __init__( | |
self, | |
client_identifier: Optional[str] = None, | |
ja3_string: Optional[str] = None, | |
h2_settings: Optional[dict] = None, # Optional[dict[str, int]] | |
h2_settings_order: Optional[list] = None, # Optional[list[str]] | |
supported_signature_algorithms: Optional[list] = None, # Optional[list[str]] | |
supported_versions: Optional[list] = None, # Optional[list[str]] | |
key_share_curves: Optional[list] = None, # Optional[list[str]] | |
cert_compression_algo: str = None, | |
pseudo_header_order: Optional[list] = None, # Optional[list[str] | |
connection_flow: Optional[int] = None, | |
priority_frames: Optional[list] = None, | |
header_order: Optional[list] = None, # Optional[list[str]] | |
header_priority: Optional[dict] = None, # Optional[list[str]] | |
random_tls_extension_order: Optional = False, | |
force_http1: Optional = False, | |
) -> None: | |
self._session_id = str(uuid.uuid4()) | |
# --- Standard Settings ---------------------------------------------------------------------------------------- | |
# Case-insensitive dictionary of headers, send on each request | |
self.headers = CaseInsensitiveDict( | |
{ | |
"User-Agent": f"tls-client/{__version__}", | |
"Accept-Encoding": "gzip, deflate, br", | |
"Accept": "*/*", | |
"Connection": "keep-alive", | |
} | |
) | |
# Example: | |
# { | |
# "http": "http://user:pass@ip:port", | |
# "https": "http://user:pass@ip:port" | |
# } | |
self.proxies = {} | |
# Dictionary of querystring data to attach to each request. The dictionary values may be lists for representing | |
# multivalued query parameters. | |
self.params = {} | |
# CookieJar containing all currently outstanding cookies set on this session | |
self.cookies = cookiejar_from_dict({}) | |
# --- Advanced Settings ---------------------------------------------------------------------------------------- | |
# Examples: | |
# Chrome --> chrome_103, chrome_104, chrome_105, chrome_106 | |
# Firefox --> firefox_102, firefox_104 | |
# Opera --> opera_89, opera_90 | |
# Safari --> safari_15_3, safari_15_6_1, safari_16_0 | |
# iOS --> safari_ios_15_5, safari_ios_15_6, safari_ios_16_0 | |
# iPadOS --> safari_ios_15_6 | |
self.client_identifier = client_identifier | |
# Set JA3 --> TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats | |
# Example: | |
# 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 | |
self.ja3_string = ja3_string | |
# HTTP2 Header Frame Settings | |
# Possible Settings: | |
# HEADER_TABLE_SIZE | |
# SETTINGS_ENABLE_PUSH | |
# MAX_CONCURRENT_STREAMS | |
# INITIAL_WINDOW_SIZE | |
# MAX_FRAME_SIZE | |
# MAX_HEADER_LIST_SIZE | |
# | |
# Example: | |
# { | |
# "HEADER_TABLE_SIZE": 65536, | |
# "MAX_CONCURRENT_STREAMS": 1000, | |
# "INITIAL_WINDOW_SIZE": 6291456, | |
# "MAX_HEADER_LIST_SIZE": 262144 | |
# } | |
self.h2_settings = h2_settings | |
# HTTP2 Header Frame Settings Order | |
# Example: | |
# [ | |
# "HEADER_TABLE_SIZE", | |
# "MAX_CONCURRENT_STREAMS", | |
# "INITIAL_WINDOW_SIZE", | |
# "MAX_HEADER_LIST_SIZE" | |
# ] | |
self.h2_settings_order = h2_settings_order | |
# Supported Signature Algorithms | |
# Possible Settings: | |
# PKCS1WithSHA256 | |
# PKCS1WithSHA384 | |
# PKCS1WithSHA512 | |
# PSSWithSHA256 | |
# PSSWithSHA384 | |
# PSSWithSHA512 | |
# ECDSAWithP256AndSHA256 | |
# ECDSAWithP384AndSHA384 | |
# ECDSAWithP521AndSHA512 | |
# PKCS1WithSHA1 | |
# ECDSAWithSHA1 | |
# | |
# Example: | |
# [ | |
# "ECDSAWithP256AndSHA256", | |
# "PSSWithSHA256", | |
# "PKCS1WithSHA256", | |
# "ECDSAWithP384AndSHA384", | |
# "PSSWithSHA384", | |
# "PKCS1WithSHA384", | |
# "PSSWithSHA512", | |
# "PKCS1WithSHA512", | |
# ] | |
self.supported_signature_algorithms = supported_signature_algorithms | |
# Supported Versions | |
# Possible Settings: | |
# GREASE | |
# 1.3 | |
# 1.2 | |
# 1.1 | |
# 1.0 | |
# | |
# Example: | |
# [ | |
# "GREASE", | |
# "1.3", | |
# "1.2" | |
# ] | |
self.supported_versions = supported_versions | |
# Key Share Curves | |
# Possible Settings: | |
# GREASE | |
# P256 | |
# P384 | |
# P521 | |
# X25519 | |
# | |
# Example: | |
# [ | |
# "GREASE", | |
# "X25519" | |
# ] | |
self.key_share_curves = key_share_curves | |
# Cert Compression Algorithm | |
# Examples: "zlib", "brotli", "zstd" | |
self.cert_compression_algo = cert_compression_algo | |
# Pseudo Header Order (:authority, :method, :path, :scheme) | |
# Example: | |
# [ | |
# ":method", | |
# ":authority", | |
# ":scheme", | |
# ":path" | |
# ] | |
self.pseudo_header_order = pseudo_header_order | |
# Connection Flow / Window Size Increment | |
# Example: | |
# 15663105 | |
self.connection_flow = connection_flow | |
# Example: | |
# [ | |
# { | |
# "streamID": 3, | |
# "priorityParam": { | |
# "weight": 201, | |
# "streamDep": 0, | |
# "exclusive": false | |
# } | |
# }, | |
# { | |
# "streamID": 5, | |
# "priorityParam": { | |
# "weight": 101, | |
# "streamDep": false, | |
# "exclusive": 0 | |
# } | |
# } | |
# ] | |
self.priority_frames = priority_frames | |
# Order of your headers | |
# Example: | |
# [ | |
# "key1", | |
# "key2" | |
# ] | |
self.header_order = header_order | |
# Header Priority | |
# Example: | |
# { | |
# "streamDep": 1, | |
# "exclusive": true, | |
# "weight": 1 | |
# } | |
self.header_priority = header_priority | |
# randomize tls extension order | |
self.random_tls_extension_order = random_tls_extension_order | |
# force HTTP1 | |
self.force_http1 = force_http1 | |
def execute_request( | |
self, | |
method: str, | |
url: str, | |
params: Optional[dict] = None, # Optional[dict[str, str]] | |
data: Optional[Union[str, dict]] = None, | |
headers: Optional[dict] = None, # Optional[dict[str, str]] | |
cookies: Optional[dict] = None, # Optional[dict[str, str]] | |
json: Optional[dict] = None, # Optional[dict] | |
allow_redirects: Optional[bool] = False, | |
insecure_skip_verify: Optional[bool] = False, | |
timeout_seconds: Optional[int] = 30, | |
proxy: Optional[dict] = None # Optional[dict[str, str]] | |
): | |
# --- URL ------------------------------------------------------------------------------------------------------ | |
# Prepare URL - add params to url | |
if params is not None: | |
url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}" | |
# --- Request Body --------------------------------------------------------------------------------------------- | |
# Prepare request body - build request body | |
# Data has priority. JSON is only used if data is None. | |
if data is None and json is not None: | |
if type(json) in [dict, list]: | |
json = dumps(json) | |
request_body = json | |
content_type = "application/json" | |
elif data is not None and type(data) not in [str, bytes]: | |
request_body = urllib.parse.urlencode(data, doseq=True) | |
content_type = "application/x-www-form-urlencoded" | |
else: | |
request_body = data | |
content_type = None | |
# set content type if it isn't set | |
if content_type is not None and "content-type" not in self.headers: | |
self.headers["Content-Type"] = content_type | |
# --- Headers -------------------------------------------------------------------------------------------------- | |
# merge headers of session and of the request | |
if headers is not None: | |
for header_key, header_value in headers.items(): | |
# check if all header keys and values are strings | |
if type(header_key) is str and type(header_value) is str: | |
self.headers[header_key] = header_value | |
headers = self.headers | |
else: | |
headers = self.headers | |
# --- Cookies -------------------------------------------------------------------------------------------------- | |
cookies = cookies or {} | |
# Merge with session cookies | |
cookies = merge_cookies(self.cookies, cookies) | |
cookie_header = get_cookie_header( | |
request_url=url, | |
request_headers=headers, | |
cookie_jar=cookies | |
) | |
if cookie_header is not None: | |
headers["Cookie"] = cookie_header | |
# --- Proxy ---------------------------------------------------------------------------------------------------- | |
proxy = proxy or self.proxies | |
if type(proxy) is dict and "http" in proxy: | |
proxy = proxy["http"] | |
elif type(proxy) is str: | |
proxy = proxy | |
else: | |
proxy = "" | |
# --- Request -------------------------------------------------------------------------------------------------- | |
is_byte_request = isinstance(request_body, (bytes, bytearray)) | |
request_payload = { | |
"sessionId": self._session_id, | |
"followRedirects": allow_redirects, | |
"forceHttp1": self.force_http1, | |
"headers": dict(headers), | |
"headerOrder": self.header_order, | |
"insecureSkipVerify": insecure_skip_verify, | |
"isByteRequest": is_byte_request, | |
"proxyUrl": proxy, | |
"requestUrl": url, | |
"requestMethod": method, | |
"requestBody": base64.b64encode(request_body).decode() if is_byte_request else request_body, | |
"requestCookies": [], # Empty because it's handled in python | |
"timeoutSeconds": timeout_seconds, | |
} | |
if self.client_identifier is None: | |
request_payload["customTlsClient"] = { | |
"ja3String": self.ja3_string, | |
"h2Settings": self.h2_settings, | |
"h2SettingsOrder": self.h2_settings_order, | |
"pseudoHeaderOrder": self.pseudo_header_order, | |
"connectionFlow": self.connection_flow, | |
"priorityFrames": self.priority_frames, | |
"headerPriority": self.header_priority, | |
"certCompressionAlgo": self.cert_compression_algo, | |
"supportedVersions": self.supported_versions, | |
"supportedSignatureAlgorithms": self.supported_signature_algorithms, | |
"keyShareCurves": self.key_share_curves, | |
} | |
else: | |
request_payload["tlsClientIdentifier"] = self.client_identifier | |
request_payload["withRandomTLSExtensionOrder"] = self.random_tls_extension_order | |
# this is a pointer to the response | |
response = request(dumps(request_payload).encode('utf-8')) | |
# dereference the pointer to a byte array | |
response_bytes = ctypes.string_at(response) | |
# convert our byte array to a string (tls client returns json) | |
response_string = response_bytes.decode('utf-8') | |
# convert response string to json | |
response_object = loads(response_string) | |
# --- Response ------------------------------------------------------------------------------------------------- | |
# Error handling | |
if response_object["status"] == 0: | |
raise TLSClientExeption(response_object["body"]) | |
# Set response cookies | |
response_cookie_jar = extract_cookies_to_jar( | |
request_url=url, | |
request_headers=headers, | |
cookie_jar=cookies, | |
response_headers=response_object["headers"] | |
) | |
# build response class | |
return build_response(response_object, response_cookie_jar) | |
def get( | |
self, | |
url: str, | |
**kwargs: Any | |
): | |
"""Sends a GET request""" | |
return self.execute_request(method="GET", url=url, **kwargs) | |
def options( | |
self, | |
url: str, | |
**kwargs: Any | |
): | |
"""Sends a OPTIONS request""" | |
return self.execute_request(method="OPTIONS", url=url, **kwargs) | |
def head( | |
self, | |
url: str, | |
**kwargs: Any | |
): | |
"""Sends a HEAD request""" | |
return self.execute_request(method="HEAD", url=url, **kwargs) | |
def post( | |
self, | |
url: str, | |
data: Optional[Union[str, dict]] = None, | |
json: Optional[dict] = None, | |
**kwargs: Any | |
): | |
"""Sends a POST request""" | |
return self.execute_request(method="POST", url=url, data=data, json=json, **kwargs) | |
def put( | |
self, | |
url: str, | |
data: Optional[Union[str, dict]] = None, | |
json: Optional[dict] = None, | |
**kwargs: Any | |
): | |
"""Sends a PUT request""" | |
return self.execute_request(method="PUT", url=url, data=data, json=json, **kwargs) | |
def patch( | |
self, | |
url: str, | |
data: Optional[Union[str, dict]] = None, | |
json: Optional[dict] = None, | |
**kwargs: Any | |
): | |
"""Sends a PUT request""" | |
return self.execute_request(method="PATCH", url=url, data=data, json=json, **kwargs) | |
def delete( | |
self, | |
url: str, | |
**kwargs: Any | |
): | |
"""Sends a DELETE request""" | |
return self.execute_request(method="DELETE", url=url, **kwargs) | |