├── LICENSE ├── README.md ├── examples ├── example1 - preset.py ├── example2 - custom.py └── example3- certificate pinning.py ├── requirements.txt ├── scripts ├── build.bat └── update_shared_libraries.py ├── setup.py └── tls_client ├── __init__.py ├── __version__.py ├── cffi.py ├── cookies.py ├── dependencies ├── __init__.py ├── tls-client-32.dll ├── tls-client-64.dll ├── tls-client-amd64.so ├── tls-client-arm64.dylib ├── tls-client-arm64.so ├── tls-client-x86.dylib └── tls-client-x86.so ├── exceptions.py ├── response.py ├── sessions.py ├── settings.py └── structures.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian Zager 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-TLS-Client 2 | Python-TLS-Client is an advanced HTTP library based on requests and tls-client. 3 | 4 | # Installation 5 | ``` 6 | pip install tls-client 7 | ``` 8 | 9 | # Examples 10 | The syntax is inspired by [requests](https://github.com/psf/requests), so its very similar and there are only very few things that are different. 11 | 12 | Example 1 - Preset: 13 | ```python 14 | import tls_client 15 | 16 | # You can also use the following as `client_identifier`: 17 | # Chrome --> chrome_103, chrome_104, chrome_105, chrome_106, chrome_107, chrome_108, chrome109, Chrome110, 18 | # chrome111, chrome112, chrome_116_PSK, chrome_116_PSK_PQ, chrome_117, chrome_120 19 | # Firefox --> firefox_102, firefox_104, firefox108, Firefox110, firefox_117, firefox_120 20 | # Opera --> opera_89, opera_90 21 | # Safari --> safari_15_3, safari_15_6_1, safari_16_0 22 | # iOS --> safari_ios_15_5, safari_ios_15_6, safari_ios_16_0 23 | # iPadOS --> safari_ios_15_6 24 | # Android --> okhttp4_android_7, okhttp4_android_8, okhttp4_android_9, okhttp4_android_10, okhttp4_android_11, 25 | # okhttp4_android_12, okhttp4_android_13 26 | # 27 | # more client identifiers can be found in settings.py 28 | 29 | session = tls_client.Session( 30 | client_identifier="chrome112", 31 | random_tls_extension_order=True 32 | ) 33 | 34 | res = session.get( 35 | "https://www.example.com/", 36 | headers={ 37 | "key1": "value1", 38 | }, 39 | proxy="http://user:password@host:port" 40 | ) 41 | ``` 42 | 43 | Example 2 - Custom: 44 | ```python 45 | import tls_client 46 | 47 | session = tls_client.Session( 48 | ja3_string="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", 49 | h2_settings={ 50 | "HEADER_TABLE_SIZE": 65536, 51 | "MAX_CONCURRENT_STREAMS": 1000, 52 | "INITIAL_WINDOW_SIZE": 6291456, 53 | "MAX_HEADER_LIST_SIZE": 262144 54 | }, 55 | h2_settings_order=[ 56 | "HEADER_TABLE_SIZE", 57 | "MAX_CONCURRENT_STREAMS", 58 | "INITIAL_WINDOW_SIZE", 59 | "MAX_HEADER_LIST_SIZE" 60 | ], 61 | supported_signature_algorithms=[ 62 | "ECDSAWithP256AndSHA256", 63 | "PSSWithSHA256", 64 | "PKCS1WithSHA256", 65 | "ECDSAWithP384AndSHA384", 66 | "PSSWithSHA384", 67 | "PKCS1WithSHA384", 68 | "PSSWithSHA512", 69 | "PKCS1WithSHA512", 70 | ], 71 | supported_versions=["GREASE", "1.3", "1.2"], 72 | key_share_curves=["GREASE", "X25519"], 73 | cert_compression_algo="brotli", 74 | pseudo_header_order=[ 75 | ":method", 76 | ":authority", 77 | ":scheme", 78 | ":path" 79 | ], 80 | connection_flow=15663105, 81 | header_order=[ 82 | "accept", 83 | "user-agent", 84 | "accept-encoding", 85 | "accept-language" 86 | ] 87 | ) 88 | 89 | res = session.post( 90 | "https://www.example.com/", 91 | headers={ 92 | "key1": "value1", 93 | }, 94 | json={ 95 | "key1": "key2" 96 | } 97 | ) 98 | ``` 99 | 100 | # Pyinstaller / Pyarmor 101 | **If you want to pack the library with Pyinstaller or Pyarmor, make sure to add this to your command:** 102 | 103 | Linux - Ubuntu / x86: 104 | ``` 105 | --add-binary '{path_to_library}/tls_client/dependencies/tls-client-x86.so:tls_client/dependencies' 106 | ``` 107 | 108 | Linux Alpine / AMD64: 109 | ``` 110 | --add-binary '{path_to_library}/tls_client/dependencies/tls-client-amd64.so:tls_client/dependencies' 111 | ``` 112 | 113 | MacOS M1 and older: 114 | ``` 115 | --add-binary '{path_to_library}/tls_client/dependencies/tls-client-x86.dylib:tls_client/dependencies' 116 | ``` 117 | 118 | MacOS M2: 119 | ``` 120 | --add-binary '{path_to_library}/tls_client/dependencies/tls-client-arm64.dylib:tls_client/dependencies' 121 | ``` 122 | 123 | Windows: 124 | ``` 125 | --add-binary '{path_to_library}/tls_client/dependencies/tls-client-64.dll;tls_client/dependencies' 126 | ``` 127 | 128 | # Acknowledgements 129 | Big shout out to [Bogdanfinn](https://github.com/bogdanfinn) for open sourcing his [tls-client](https://github.com/bogdanfinn/tls-client) in Golang. 130 | Also I wanted to keep the syntax as similar as possible to [requests](https://github.com/psf/requests), as most people use it and are familiar with it! 131 | -------------------------------------------------------------------------------- /examples/example1 - preset.py: -------------------------------------------------------------------------------- 1 | import tls_client 2 | 3 | # You can also use the following as `client_identifier`: 4 | # Chrome --> chrome_103, chrome_104, chrome_105, chrome_106, chrome_107, chrome_108, chrome109, Chrome110, 5 | # chrome111, chrome112, chrome_116_PSK, chrome_116_PSK_PQ, chrome_117, chrome_120 6 | # Firefox --> firefox_102, firefox_104, firefox108, Firefox110, firefox_117, firefox_120 7 | # Opera --> opera_89, opera_90 8 | # Safari --> safari_15_3, safari_15_6_1, safari_16_0 9 | # iOS --> safari_ios_15_5, safari_ios_15_6, safari_ios_16_0 10 | # iPadOS --> safari_ios_15_6 11 | # Android --> okhttp4_android_7, okhttp4_android_8, okhttp4_android_9, okhttp4_android_10, okhttp4_android_11, 12 | # okhttp4_android_12, okhttp4_android_13 13 | # 14 | # more client identifiers can be found in settings.py 15 | 16 | session = tls_client.Session( 17 | client_identifier="chrome112", 18 | random_tls_extension_order=True 19 | ) 20 | 21 | res = session.get( 22 | "https://www.example.com/", 23 | headers={ 24 | "key1": "value1", 25 | }, 26 | proxy="http://user:password@host:port" 27 | ) -------------------------------------------------------------------------------- /examples/example2 - custom.py: -------------------------------------------------------------------------------- 1 | import tls_client 2 | 3 | # You can find more details about the arguments in `session.py` e.g. what 1, 2, 3, 4 etc. represents in h2_settings 4 | session = tls_client.Session( 5 | ja3_string="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", 6 | h2_settings={ 7 | "HEADER_TABLE_SIZE": 65536, 8 | "MAX_CONCURRENT_STREAMS": 1000, 9 | "INITIAL_WINDOW_SIZE": 6291456, 10 | "MAX_HEADER_LIST_SIZE": 262144 11 | }, 12 | h2_settings_order=[ 13 | "HEADER_TABLE_SIZE", 14 | "MAX_CONCURRENT_STREAMS", 15 | "INITIAL_WINDOW_SIZE", 16 | "MAX_HEADER_LIST_SIZE" 17 | ], 18 | supported_signature_algorithms=[ 19 | "ECDSAWithP256AndSHA256", 20 | "PSSWithSHA256", 21 | "PKCS1WithSHA256", 22 | "ECDSAWithP384AndSHA384", 23 | "PSSWithSHA384", 24 | "PKCS1WithSHA384", 25 | "PSSWithSHA512", 26 | "PKCS1WithSHA512", 27 | ], 28 | supported_versions=["GREASE", "1.3", "1.2"], 29 | key_share_curves=["GREASE", "X25519"], 30 | cert_compression_algo="brotli", 31 | pseudo_header_order=[ 32 | ":method", 33 | ":authority", 34 | ":scheme", 35 | ":path" 36 | ], 37 | connection_flow=15663105, 38 | header_order=[ 39 | "accept", 40 | "user-agent", 41 | "accept-encoding", 42 | "accept-language" 43 | ] 44 | ) 45 | 46 | res = session.post( 47 | "https://www.example.com/", 48 | headers={ 49 | "key1": "value1", 50 | }, 51 | json={ 52 | "key1": "key2" 53 | } 54 | ) -------------------------------------------------------------------------------- /examples/example3- certificate pinning.py: -------------------------------------------------------------------------------- 1 | import tls_client 2 | 3 | session = tls_client.Session( 4 | certificate_pinning={ 5 | "example.com": [ 6 | "NQvyabcS99nBqk/nZCUF44hFhshrkvxqYtfrZq3i+Ww=", 7 | "4a6cdefI7OG6cuDZka5NDZ7FR8a60d3auda+sKfg4Ng=", 8 | "x4QzuiC810K5/cMjb05Qm4k3Bw5zBn4lTdO/nEW/Td4=" 9 | ] 10 | } 11 | ) 12 | 13 | res = session.get( 14 | "https://www.example.com/", 15 | headers={ 16 | "key1": "value1", 17 | }, 18 | proxy="http://user:password@host:port" 19 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing-extensions -------------------------------------------------------------------------------- /scripts/build.bat: -------------------------------------------------------------------------------- 1 | python setup.py sdist bdist_wheel 2 | twine upload dist/* --verbose -------------------------------------------------------------------------------- /scripts/update_shared_libraries.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | shared_library_version = "1.7.2" 4 | github_download_url = "https://github.com//bogdanfinn/tls-client/releases/download/v{}/{}" 5 | github_repo_filenames = [ 6 | # Windows 7 | f"tls-client-windows-32-v{shared_library_version}.dll", 8 | f"tls-client-windows-64-v{shared_library_version}.dll", 9 | # MacOS 10 | f"tls-client-darwin-arm64-v{shared_library_version}.dylib", 11 | f"tls-client-darwin-amd64-v{shared_library_version}.dylib", 12 | # Linux 13 | f"tls-client-linux-alpine-amd64-v{shared_library_version}.so", 14 | f"tls-client-linux-ubuntu-amd64-v{shared_library_version}.so", 15 | f"tls-client-linux-arm64-v{shared_library_version}.so" 16 | ] 17 | dependency_filenames = [ 18 | # Windows 19 | "tls-client-32.dll", 20 | "tls-client-64.dll", 21 | # MacOS 22 | "tls-client-arm64.dylib", 23 | "tls-client-x86.dylib", 24 | # Linux 25 | "tls-client-amd64.so", 26 | "tls-client-x86.so", 27 | "tls-client-arm64.so" 28 | ] 29 | 30 | for github_filename, dependency_filename in zip(github_repo_filenames, dependency_filenames): 31 | response = requests.get( 32 | url=github_download_url.format(shared_library_version, github_filename) 33 | ) 34 | 35 | with open(f"../tls_client/dependencies/{dependency_filename}", "wb") as f: 36 | f.write(response.content) 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | from codecs import open 4 | import glob 5 | import sys 6 | import os 7 | 8 | 9 | data_files = [] 10 | directories = glob.glob('tls_client/dependencies/') 11 | for directory in directories: 12 | files = glob.glob(directory+'*') 13 | data_files.append(('tls_client/dependencies', files)) 14 | 15 | about = {} 16 | here = os.path.abspath(os.path.dirname(__file__)) 17 | with open(os.path.join(here, "tls_client", "__version__.py"), "r", "utf-8") as f: 18 | exec(f.read(), about) 19 | 20 | with open("README.md", "r", "utf-8") as f: 21 | readme = f.read() 22 | 23 | setup( 24 | name=about["__title__"], 25 | version=about["__version__"], 26 | author=about["__author__"], 27 | description=about["__description__"], 28 | license=about["__license__"], 29 | long_description=readme, 30 | long_description_content_type="text/markdown", 31 | packages=find_packages(), 32 | include_package_data=True, 33 | package_data={ 34 | '': ['*'], 35 | }, 36 | classifiers=[ 37 | "Environment :: Web Environment", 38 | "Intended Audience :: Developers", 39 | "Natural Language :: English", 40 | "Operating System :: Unix", 41 | "Operating System :: MacOS :: MacOS X", 42 | "Operating System :: Microsoft :: Windows", 43 | "Programming Language :: Python", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.7", 46 | "Programming Language :: Python :: 3.8", 47 | "Programming Language :: Python :: 3.9", 48 | "Programming Language :: Python :: 3.10", 49 | "Programming Language :: Python :: 3.11", 50 | "Programming Language :: Python :: 3.12", 51 | "Programming Language :: Python :: 3 :: Only", 52 | "Topic :: Internet :: WWW/HTTP", 53 | "Topic :: Software Development :: Libraries", 54 | ], 55 | project_urls={ 56 | "Source": "https://github.com/FlorianREGAZ/Python-Tls-Client", 57 | } 58 | ) -------------------------------------------------------------------------------- /tls_client/__init__.py: -------------------------------------------------------------------------------- 1 | # _____ __ __ ___ _ _ _ 2 | # /__ \/ / / _\ / __\ (_) ___ _ __ | |_ 3 | # / /\/ / \ \ _____ / / | | |/ _ \ '_ \| __| 4 | # / / / /____\ \_____/ /___| | | __/ | | | |_ 5 | # \/ \____/\__/ \____/|_|_|\___|_| |_|\__| 6 | 7 | # Disclaimer: 8 | # Big shout out to Bogdanfinn for open sourcing his tls-client in Golang. 9 | # Also to requests, as most of the cookie handling is copied from it. :'D 10 | # I wanted to keep the syntax as similar as possible to requests, as most people use it and are familiar with it! 11 | # Links: 12 | # tls-client: https://github.com/bogdanfinn/tls-client 13 | # requests: https://github.com/psf/requests 14 | 15 | from .sessions import Session -------------------------------------------------------------------------------- /tls_client/__version__.py: -------------------------------------------------------------------------------- 1 | # _____ __ __ ___ _ _ _ 2 | # /__ \/ / / _\ / __\ (_) ___ _ __ | |_ 3 | # / /\/ / \ \ _____ / / | | |/ _ \ '_ \| __| 4 | # / / / /____\ \_____/ /___| | | __/ | | | |_ 5 | # \/ \____/\__/ \____/|_|_|\___|_| |_|\__| 6 | 7 | __title__ = "tls_client" 8 | __description__ = "Advanced Python HTTP Client." 9 | __version__ = "1.0.1" 10 | __author__ = "Florian Zager" 11 | __license__ = "MIT" -------------------------------------------------------------------------------- /tls_client/cffi.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | from platform import machine 3 | import ctypes 4 | import os 5 | 6 | 7 | if platform == 'darwin': 8 | file_ext = '-arm64.dylib' if machine() == "arm64" else '-x86.dylib' 9 | elif platform in ('win32', 'cygwin'): 10 | file_ext = '-64.dll' if 8 == ctypes.sizeof(ctypes.c_voidp) else '-32.dll' 11 | else: 12 | if machine() == "aarch64": 13 | file_ext = '-arm64.so' 14 | elif "x86" in machine(): 15 | file_ext = '-x86.so' 16 | else: 17 | file_ext = '-amd64.so' 18 | 19 | root_dir = os.path.abspath(os.path.dirname(__file__)) 20 | library = ctypes.cdll.LoadLibrary(f'{root_dir}/dependencies/tls-client{file_ext}') 21 | 22 | # extract the exposed request function from the shared package 23 | request = library.request 24 | request.argtypes = [ctypes.c_char_p] 25 | request.restype = ctypes.c_char_p 26 | 27 | freeMemory = library.freeMemory 28 | freeMemory.argtypes = [ctypes.c_char_p] 29 | freeMemory.restype = ctypes.c_char_p 30 | 31 | destroySession = library.destroySession 32 | destroySession.argtypes = [ctypes.c_char_p] 33 | destroySession.restype = ctypes.c_char_p 34 | -------------------------------------------------------------------------------- /tls_client/cookies.py: -------------------------------------------------------------------------------- 1 | from .structures import CaseInsensitiveDict 2 | 3 | from http.cookiejar import CookieJar, Cookie 4 | from typing import MutableMapping, Union, Any 5 | from urllib.parse import urlparse, urlunparse 6 | from http.client import HTTPMessage 7 | import copy 8 | 9 | try: 10 | import threading 11 | except ImportError: 12 | import dummy_threading as threading 13 | 14 | 15 | class MockRequest: 16 | """ 17 | Mimic a urllib2.Request to get the correct cookie string for the request. 18 | """ 19 | 20 | def __init__(self, request_url: str, request_headers: CaseInsensitiveDict): 21 | self.request_url = request_url 22 | self.request_headers = request_headers 23 | self._new_headers = {} 24 | self.type = urlparse(self.request_url).scheme 25 | 26 | def get_type(self): 27 | return self.type 28 | 29 | def get_host(self): 30 | return urlparse(self.request_url).netloc 31 | 32 | def get_origin_req_host(self): 33 | return self.get_host() 34 | 35 | def get_full_url(self): 36 | # Only return the response's URL if the user hadn't set the Host 37 | # header 38 | if not self.request_headers.get("Host"): 39 | return self.request_url 40 | # If they did set it, retrieve it and reconstruct the expected domain 41 | host = self.request_headers["Host"] 42 | parsed = urlparse(self.request_url) 43 | # Reconstruct the URL as we expect it 44 | return urlunparse( 45 | [ 46 | parsed.scheme, 47 | host, 48 | parsed.path, 49 | parsed.params, 50 | parsed.query, 51 | parsed.fragment, 52 | ] 53 | ) 54 | 55 | def is_unverifiable(self): 56 | return True 57 | 58 | def has_header(self, name): 59 | return name in self.request_headers or name in self._new_headers 60 | 61 | def get_header(self, name, default=None): 62 | return self.request_headers.get(name, self._new_headers.get(name, default)) 63 | 64 | def add_unredirected_header(self, name, value): 65 | self._new_headers[name] = value 66 | 67 | def get_new_headers(self): 68 | return self._new_headers 69 | 70 | @property 71 | def unverifiable(self): 72 | return self.is_unverifiable() 73 | 74 | @property 75 | def origin_req_host(self): 76 | return self.get_origin_req_host() 77 | 78 | @property 79 | def host(self): 80 | return self.get_host() 81 | 82 | 83 | class MockResponse: 84 | """ 85 | Wraps a httplib.HTTPMessage to mimic a urllib.addinfourl. 86 | The objective is to retrieve the response cookies correctly. 87 | """ 88 | 89 | def __init__(self, headers): 90 | self._headers = headers 91 | 92 | def info(self): 93 | return self._headers 94 | 95 | def getheaders(self, name): 96 | self._headers.getheaders(name) 97 | 98 | 99 | class CookieConflictError(RuntimeError): 100 | """There are two cookies that meet the criteria specified in the cookie jar. 101 | Use .get and .set and include domain and path args in order to be more specific. 102 | """ 103 | 104 | 105 | class RequestsCookieJar(CookieJar, MutableMapping): 106 | """ Origin: requests library (https://github.com/psf/requests) 107 | Compatibility class; is a cookielib.CookieJar, but exposes a dict 108 | interface. 109 | 110 | This is the CookieJar we create by default for requests and sessions that 111 | don't specify one, since some clients may expect response.cookies and 112 | session.cookies to support dict operations. 113 | 114 | Requests does not use the dict interface internally; it's just for 115 | compatibility with external client code. All requests code should work 116 | out of the box with externally provided instances of ``CookieJar``, e.g. 117 | ``LWPCookieJar`` and ``FileCookieJar``. 118 | 119 | Unlike a regular CookieJar, this class is pickleable. 120 | 121 | .. warning:: dictionary operations that are normally O(1) may be O(n). 122 | """ 123 | 124 | def get(self, name, default=None, domain=None, path=None): 125 | """Dict-like get() that also supports optional domain and path args in 126 | order to resolve naming collisions from using one cookie jar over 127 | multiple domains. 128 | 129 | .. warning:: operation is O(n), not O(1). 130 | """ 131 | try: 132 | return self._find_no_duplicates(name, domain, path) 133 | except KeyError: 134 | return default 135 | 136 | def set(self, name, value, **kwargs): 137 | """Dict-like set() that also supports optional domain and path args in 138 | order to resolve naming collisions from using one cookie jar over 139 | multiple domains. 140 | """ 141 | # support client code that unsets cookies by assignment of a None value: 142 | if value is None: 143 | remove_cookie_by_name( 144 | self, name, domain=kwargs.get("domain"), path=kwargs.get("path") 145 | ) 146 | return 147 | 148 | c = create_cookie(name, value, **kwargs) 149 | self.set_cookie(c) 150 | return c 151 | 152 | def iterkeys(self): 153 | """Dict-like iterkeys() that returns an iterator of names of cookies 154 | from the jar. 155 | 156 | .. seealso:: itervalues() and iteritems(). 157 | """ 158 | for cookie in iter(self): 159 | yield cookie.name 160 | 161 | def keys(self): 162 | """Dict-like keys() that returns a list of names of cookies from the 163 | jar. 164 | 165 | .. seealso:: values() and items(). 166 | """ 167 | return list(self.iterkeys()) 168 | 169 | def itervalues(self): 170 | """Dict-like itervalues() that returns an iterator of values of cookies 171 | from the jar. 172 | 173 | .. seealso:: iterkeys() and iteritems(). 174 | """ 175 | for cookie in iter(self): 176 | yield cookie.value 177 | 178 | def values(self): 179 | """Dict-like values() that returns a list of values of cookies from the 180 | jar. 181 | 182 | .. seealso:: keys() and items(). 183 | """ 184 | return list(self.itervalues()) 185 | 186 | def iteritems(self): 187 | """Dict-like iteritems() that returns an iterator of name-value tuples 188 | from the jar. 189 | 190 | .. seealso:: iterkeys() and itervalues(). 191 | """ 192 | for cookie in iter(self): 193 | yield cookie.name, cookie.value 194 | 195 | def items(self): 196 | """Dict-like items() that returns a list of name-value tuples from the 197 | jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a 198 | vanilla python dict of key value pairs. 199 | 200 | .. seealso:: keys() and values(). 201 | """ 202 | return list(self.iteritems()) 203 | 204 | def list_domains(self): 205 | """Utility method to list all the domains in the jar.""" 206 | domains = [] 207 | for cookie in iter(self): 208 | if cookie.domain not in domains: 209 | domains.append(cookie.domain) 210 | return domains 211 | 212 | def list_paths(self): 213 | """Utility method to list all the paths in the jar.""" 214 | paths = [] 215 | for cookie in iter(self): 216 | if cookie.path not in paths: 217 | paths.append(cookie.path) 218 | return paths 219 | 220 | def multiple_domains(self): 221 | """Returns True if there are multiple domains in the jar. 222 | Returns False otherwise. 223 | 224 | :rtype: bool 225 | """ 226 | domains = [] 227 | for cookie in iter(self): 228 | if cookie.domain is not None and cookie.domain in domains: 229 | return True 230 | domains.append(cookie.domain) 231 | return False # there is only one domain in jar 232 | 233 | def get_dict(self, domain=None, path=None): 234 | """Takes as an argument an optional domain and path and returns a plain 235 | old Python dict of name-value pairs of cookies that meet the 236 | requirements. 237 | 238 | :rtype: dict 239 | """ 240 | dictionary = {} 241 | for cookie in iter(self): 242 | if (domain is None or cookie.domain == domain) and ( 243 | path is None or cookie.path == path 244 | ): 245 | dictionary[cookie.name] = cookie.value 246 | return dictionary 247 | 248 | def __contains__(self, name): 249 | try: 250 | return super().__contains__(name) 251 | except CookieConflictError: 252 | return True 253 | 254 | def __getitem__(self, name): 255 | """Dict-like __getitem__() for compatibility with client code. Throws 256 | exception if there are more than one cookie with name. In that case, 257 | use the more explicit get() method instead. 258 | 259 | .. warning:: operation is O(n), not O(1). 260 | """ 261 | return self._find_no_duplicates(name) 262 | 263 | def __setitem__(self, name, value): 264 | """Dict-like __setitem__ for compatibility with client code. Throws 265 | exception if there is already a cookie of that name in the jar. In that 266 | case, use the more explicit set() method instead. 267 | """ 268 | self.set(name, value) 269 | 270 | def __delitem__(self, name): 271 | """Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s 272 | ``remove_cookie_by_name()``. 273 | """ 274 | remove_cookie_by_name(self, name) 275 | 276 | def set_cookie(self, cookie, *args, **kwargs): 277 | if ( 278 | hasattr(cookie.value, "startswith") 279 | and cookie.value.startswith('"') 280 | and cookie.value.endswith('"') 281 | ): 282 | cookie.value = cookie.value.replace('\\"', "") 283 | return super().set_cookie(cookie, *args, **kwargs) 284 | 285 | def update(self, other): 286 | """Updates this jar with cookies from another CookieJar or dict-like""" 287 | if isinstance(other, CookieJar): 288 | for cookie in other: 289 | self.set_cookie(copy.copy(cookie)) 290 | else: 291 | super().update(other) 292 | 293 | def _find(self, name, domain=None, path=None): 294 | """Requests uses this method internally to get cookie values. 295 | 296 | If there are conflicting cookies, _find arbitrarily chooses one. 297 | See _find_no_duplicates if you want an exception thrown if there are 298 | conflicting cookies. 299 | 300 | :param name: a string containing name of cookie 301 | :param domain: (optional) string containing domain of cookie 302 | :param path: (optional) string containing path of cookie 303 | :return: cookie.value 304 | """ 305 | for cookie in iter(self): 306 | if cookie.name == name: 307 | if domain is None or cookie.domain == domain: 308 | if path is None or cookie.path == path: 309 | return cookie.value 310 | 311 | raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") 312 | 313 | def _find_no_duplicates(self, name, domain=None, path=None): 314 | """Both ``__get_item__`` and ``get`` call this function: it's never 315 | used elsewhere in Requests. 316 | 317 | :param name: a string containing name of cookie 318 | :param domain: (optional) string containing domain of cookie 319 | :param path: (optional) string containing path of cookie 320 | :raises KeyError: if cookie is not found 321 | :raises CookieConflictError: if there are multiple cookies 322 | that match name and optionally domain and path 323 | :return: cookie.value 324 | """ 325 | toReturn = None 326 | for cookie in iter(self): 327 | if cookie.name == name: 328 | if domain is None or cookie.domain == domain: 329 | if path is None or cookie.path == path: 330 | if toReturn is not None: 331 | # if there are multiple cookies that meet passed in criteria 332 | raise CookieConflictError( 333 | f"There are multiple cookies with name, {name!r}" 334 | ) 335 | # we will eventually return this as long as no cookie conflict 336 | toReturn = cookie.value 337 | 338 | if toReturn: 339 | return toReturn 340 | raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}") 341 | 342 | def __getstate__(self): 343 | """Unlike a normal CookieJar, this class is pickleable.""" 344 | state = self.__dict__.copy() 345 | # remove the unpickleable RLock object 346 | state.pop("_cookies_lock") 347 | return state 348 | 349 | def __setstate__(self, state): 350 | """Unlike a normal CookieJar, this class is pickleable.""" 351 | self.__dict__.update(state) 352 | if "_cookies_lock" not in self.__dict__: 353 | self._cookies_lock = threading.RLock() 354 | 355 | def copy(self): 356 | """Return a copy of this RequestsCookieJar.""" 357 | new_cj = RequestsCookieJar() 358 | new_cj.set_policy(self.get_policy()) 359 | new_cj.update(self) 360 | return new_cj 361 | 362 | def get_policy(self): 363 | """Return the CookiePolicy instance used.""" 364 | return self._policy 365 | 366 | 367 | def remove_cookie_by_name(cookiejar: RequestsCookieJar, name: str, domain: str = None, path: str = None): 368 | """Removes a cookie by name, by default over all domains and paths.""" 369 | clearables = [] 370 | for cookie in cookiejar: 371 | if cookie.name != name: 372 | continue 373 | if domain is not None and domain != cookie.domain: 374 | continue 375 | if path is not None and path != cookie.path: 376 | continue 377 | clearables.append((cookie.domain, cookie.path, cookie.name)) 378 | 379 | for domain, path, name in clearables: 380 | cookiejar.clear(domain, path, name) 381 | 382 | 383 | def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie: 384 | """Make a cookie from underspecified parameters.""" 385 | result = { 386 | "version": 0, 387 | "name": name, 388 | "value": value, 389 | "port": None, 390 | "domain": "", 391 | "path": "/", 392 | "secure": False, 393 | "expires": None, 394 | "discard": True, 395 | "comment": None, 396 | "comment_url": None, 397 | "rest": {"HttpOnly": None}, 398 | "rfc2109": False, 399 | } 400 | 401 | badargs = set(kwargs) - set(result) 402 | if badargs: 403 | raise TypeError( 404 | f"create_cookie() got unexpected keyword arguments: {list(badargs)}" 405 | ) 406 | 407 | result.update(kwargs) 408 | result["port_specified"] = bool(result["port"]) 409 | result["domain_specified"] = bool(result["domain"]) 410 | result["domain_initial_dot"] = result["domain"].startswith(".") 411 | result["path_specified"] = bool(result["path"]) 412 | 413 | return Cookie(**result) 414 | 415 | 416 | def cookiejar_from_dict(cookie_dict: dict) -> RequestsCookieJar: 417 | """transform a dict to CookieJar""" 418 | cookie_jar = RequestsCookieJar() 419 | if cookie_dict is not None: 420 | for name, value in cookie_dict.items(): 421 | cookie_jar.set_cookie(create_cookie(name=name, value=value)) 422 | return cookie_jar 423 | 424 | 425 | def merge_cookies(cookiejar: RequestsCookieJar, cookies: Union[dict, RequestsCookieJar]) -> RequestsCookieJar: 426 | """Merge cookies in session and cookies provided in request""" 427 | if type(cookies) is dict: 428 | cookies = cookiejar_from_dict(cookies) 429 | 430 | for cookie in cookies: 431 | cookiejar.set_cookie(cookie) 432 | 433 | return cookiejar 434 | 435 | def extract_cookies_to_jar( 436 | request_url: str, 437 | request_headers: CaseInsensitiveDict, 438 | cookie_jar: RequestsCookieJar, 439 | response_headers: dict 440 | ) -> RequestsCookieJar: 441 | response_cookie_jar = cookiejar_from_dict({}) 442 | 443 | req = MockRequest(request_url, request_headers) 444 | # mimic HTTPMessage 445 | http_message = HTTPMessage() 446 | http_message._headers = [] 447 | for header_name, header_values in response_headers.items(): 448 | for header_value in header_values: 449 | http_message._headers.append( 450 | (header_name, header_value) 451 | ) 452 | res = MockResponse(http_message) 453 | response_cookie_jar.extract_cookies(res, req) 454 | 455 | merge_cookies(cookie_jar, response_cookie_jar) 456 | return response_cookie_jar 457 | -------------------------------------------------------------------------------- /tls_client/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/__init__.py -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-32.dll -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-64.dll -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-amd64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-amd64.so -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-arm64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-arm64.dylib -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-arm64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-arm64.so -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-x86.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-x86.dylib -------------------------------------------------------------------------------- /tls_client/dependencies/tls-client-x86.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlorianREGAZ/Python-Tls-Client/ab6c7362564ba703f3c623a79304db3a686e5f91/tls_client/dependencies/tls-client-x86.so -------------------------------------------------------------------------------- /tls_client/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class TLSClientExeption(IOError): 3 | """General error with the TLS client""" -------------------------------------------------------------------------------- /tls_client/response.py: -------------------------------------------------------------------------------- 1 | from .cookies import cookiejar_from_dict, RequestsCookieJar 2 | from .structures import CaseInsensitiveDict 3 | 4 | from typing import Union 5 | import json 6 | 7 | 8 | class Response: 9 | """object, which contains the response to an HTTP request.""" 10 | 11 | def __init__(self): 12 | 13 | # Reference of URL the response is coming from (especially useful with redirects) 14 | self.url = None 15 | 16 | # Integer Code of responded HTTP Status, e.g. 404 or 200. 17 | self.status_code = None 18 | 19 | # String of responded HTTP Body. 20 | self.text = None 21 | 22 | # Case-insensitive Dictionary of Response Headers. 23 | self.headers = CaseInsensitiveDict() 24 | 25 | # A CookieJar of Cookies the server sent back. 26 | self.cookies = cookiejar_from_dict({}) 27 | 28 | self._content = False 29 | 30 | def __enter__(self): 31 | return self 32 | 33 | def __repr__(self): 34 | return f"" 35 | 36 | def json(self, **kwargs): 37 | """parse response body to json (dict/list)""" 38 | return json.loads(self.text, **kwargs) 39 | 40 | @property 41 | def content(self): 42 | """Content of the response, in bytes.""" 43 | 44 | if self._content is False: 45 | if self._content_consumed: 46 | raise RuntimeError("The content for this response was already consumed") 47 | 48 | if self.status_code == 0: 49 | self._content = None 50 | else: 51 | self._content = b"".join(self.iter_content(10 * 1024)) or b"" 52 | self._content_consumed = True 53 | return self._content 54 | 55 | 56 | def build_response(res: Union[dict, list], res_cookies: RequestsCookieJar) -> Response: 57 | """Builds a Response object """ 58 | response = Response() 59 | # Add target / url 60 | response.url = res["target"] 61 | # Add status code 62 | response.status_code = res["status"] 63 | # Add headers 64 | response_headers = {} 65 | if res["headers"] is not None: 66 | for header_key, header_value in res["headers"].items(): 67 | if len(header_value) == 1: 68 | response_headers[header_key] = header_value[0] 69 | else: 70 | response_headers[header_key] = header_value 71 | response.headers = response_headers 72 | # Add cookies 73 | response.cookies = res_cookies 74 | # Add response body 75 | response.text = res["body"] 76 | # Add response content (bytes) 77 | response._content = res["body"].encode() 78 | return response 79 | -------------------------------------------------------------------------------- /tls_client/sessions.py: -------------------------------------------------------------------------------- 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 | "additionalDecode": self.additional_decode, 401 | "proxyUrl": proxy, 402 | "requestUrl": url, 403 | "requestMethod": method, 404 | "requestBody": base64.b64encode(request_body).decode() if is_byte_request else request_body, 405 | "requestCookies": request_cookies, 406 | "timeoutSeconds": timeout_seconds, 407 | } 408 | if certificate_pinning: 409 | request_payload["certificatePinningHosts"] = certificate_pinning 410 | if self.client_identifier is None: 411 | request_payload["customTlsClient"] = { 412 | "ja3String": self.ja3_string, 413 | "h2Settings": self.h2_settings, 414 | "h2SettingsOrder": self.h2_settings_order, 415 | "pseudoHeaderOrder": self.pseudo_header_order, 416 | "connectionFlow": self.connection_flow, 417 | "priorityFrames": self.priority_frames, 418 | "headerPriority": self.header_priority, 419 | "certCompressionAlgo": self.cert_compression_algo, 420 | "supportedVersions": self.supported_versions, 421 | "supportedSignatureAlgorithms": self.supported_signature_algorithms, 422 | "supportedDelegatedCredentialsAlgorithms": self.supported_delegated_credentials_algorithms , 423 | "keyShareCurves": self.key_share_curves, 424 | } 425 | else: 426 | request_payload["tlsClientIdentifier"] = self.client_identifier 427 | request_payload["withRandomTLSExtensionOrder"] = self.random_tls_extension_order 428 | 429 | # this is a pointer to the response 430 | response = request(dumps(request_payload).encode('utf-8')) 431 | # dereference the pointer to a byte array 432 | response_bytes = ctypes.string_at(response) 433 | # convert our byte array to a string (tls client returns json) 434 | response_string = response_bytes.decode('utf-8') 435 | # convert response string to json 436 | response_object = loads(response_string) 437 | # free the memory 438 | freeMemory(response_object['id'].encode('utf-8')) 439 | # --- Response ------------------------------------------------------------------------------------------------- 440 | # Error handling 441 | if response_object["status"] == 0: 442 | raise TLSClientExeption(response_object["body"]) 443 | # Set response cookies 444 | response_cookie_jar = extract_cookies_to_jar( 445 | request_url=url, 446 | request_headers=headers, 447 | cookie_jar=cookies, 448 | response_headers=response_object["headers"] 449 | ) 450 | # build response class 451 | return build_response(response_object, response_cookie_jar) 452 | 453 | def get( 454 | self, 455 | url: str, 456 | **kwargs: Any 457 | ) -> Response: 458 | """Sends a GET request""" 459 | return self.execute_request(method="GET", url=url, **kwargs) 460 | 461 | def options( 462 | self, 463 | url: str, 464 | **kwargs: Any 465 | ) -> Response: 466 | """Sends a OPTIONS request""" 467 | return self.execute_request(method="OPTIONS", url=url, **kwargs) 468 | 469 | def head( 470 | self, 471 | url: str, 472 | **kwargs: Any 473 | ) -> Response: 474 | """Sends a HEAD request""" 475 | return self.execute_request(method="HEAD", url=url, **kwargs) 476 | 477 | def post( 478 | self, 479 | url: str, 480 | data: Optional[Union[str, dict]] = None, 481 | json: Optional[dict] = None, 482 | **kwargs: Any 483 | ) -> Response: 484 | """Sends a POST request""" 485 | return self.execute_request(method="POST", url=url, data=data, json=json, **kwargs) 486 | 487 | def put( 488 | self, 489 | url: str, 490 | data: Optional[Union[str, dict]] = None, 491 | json: Optional[dict] = None, 492 | **kwargs: Any 493 | ) -> Response: 494 | """Sends a PUT request""" 495 | return self.execute_request(method="PUT", url=url, data=data, json=json, **kwargs) 496 | 497 | def patch( 498 | self, 499 | url: str, 500 | data: Optional[Union[str, dict]] = None, 501 | json: Optional[dict] = None, 502 | **kwargs: Any 503 | ) -> Response: 504 | """Sends a PATCH request""" 505 | return self.execute_request(method="PATCH", url=url, data=data, json=json, **kwargs) 506 | 507 | def delete( 508 | self, 509 | url: str, 510 | **kwargs: Any 511 | ) -> Response: 512 | """Sends a DELETE request""" 513 | return self.execute_request(method="DELETE", url=url, **kwargs) 514 | -------------------------------------------------------------------------------- /tls_client/settings.py: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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())) 75 | --------------------------------------------------------------------------------