diff --git a/curl_cffi/requests/impersonate.py b/curl_cffi/requests/impersonate.py index 6eb31c1a..02521a74 100644 --- a/curl_cffi/requests/impersonate.py +++ b/curl_cffi/requests/impersonate.py @@ -1,7 +1,7 @@ import warnings from enum import Enum + from ..const import CurlSslVersion, CurlOpt -from ..curl import ffi, lib class BrowserType(str, Enum): @@ -216,7 +216,7 @@ class BrowserSpec: } -def toggle_extension(curl, extension_id, enable: bool): +def toggle_extension(curl, extension_id: int, enable: bool): # ECH if extension_id == 65037: if enable: @@ -236,3 +236,32 @@ def toggle_extension(curl, extension_id, enable: bool): curl.setopt(CurlOpt.SSL_ENABLE_ALPS, 1) else: curl.setopt(CurlOpt.SSL_ENABLE_ALPS, 0) + # server_name + elif extension_id == 0: + raise NotImplementedError("It's unlikely that the server_name(0) extension being changed.") + # ALPN + elif extension_id == 16: + raise NotImplementedError("It's unlikely that the ALPN(0) extension being changed.") + # status_request + elif extension_id == 5: + if enable: + pass # It's now always enabled + else: + raise NotImplementedError("This extension(5) is always on for now, it will be updated later.") + # signed_certificate_timestamp + elif extension_id == 18: + if enable: + pass # It's now always enabled + else: + raise NotImplementedError("This extension(18) is always on for now, it will be updated later.") + # session_ticket + elif extension_id == 35: + if enable: + curl.setopt(CurlOpt.SSL_ENABLE_TICKET, 1) + else: + curl.setopt(CurlOpt.SSL_ENABLE_TICKET, 0) + # padding + elif extension_id == 21: + pass + else: + raise NotImplementedError(f"This extension({extension_id}) can not be toggled for now, it may be updated later.") diff --git a/curl_cffi/requests/session.py b/curl_cffi/requests/session.py index f6fc95a2..b201587e 100644 --- a/curl_cffi/requests/session.py +++ b/curl_cffi/requests/session.py @@ -218,14 +218,18 @@ def __init__( def _toggle_extensions_by_ids(self, curl, extension_ids): default_enabled = {0, 51, 13, 43, 5, 18, 65281, 23, 10, 45, 35, 11, 16} - to_enable_ids = set(extension_ids) - default_enabled + to_enable_ids = extension_ids - default_enabled for ext_id in to_enable_ids: toggle_extension(curl, ext_id, enable=True) - to_disable_ids = default_enabled - set(extension_ids) + # print("to_enable: ", to_enable_ids) + + to_disable_ids = default_enabled - extension_ids for ext_id in to_disable_ids: toggle_extension(curl, ext_id, enable=False) + # print("to_disable: ", to_disable_ids) + def _set_ja3_options(self, curl, ja3: str): """ Detailed explanation: https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/ @@ -244,7 +248,14 @@ def _set_ja3_options(self, curl, ja3: str): curl.setopt(CurlOpt.SSL_CIPHER_LIST, ":".join(cipher_names)) - extension_ids = [int(e) for e in extensions.split("-")] + if extensions.endswith("-21"): + extensions = extensions[:-3] + warnings.warn( + "Padding(21) extension found in ja3 string, whether to add it should " + "be decided by the SSL engine. The TLS hello packet may contain " + "or not contain this extension, any of which should be correct." + ) + extension_ids = set(int(e) for e in extensions.split("-")) self._toggle_extensions_by_ids(curl, extension_ids) curl.setopt(CurlOpt.TLS_EXTENSION_ORDER, extensions) diff --git a/examples/impersonate.py b/examples/impersonate.py new file mode 100644 index 00000000..1099baf8 --- /dev/null +++ b/examples/impersonate.py @@ -0,0 +1,21 @@ +from curl_cffi import requests + + +# OKHTTP impersonatation examples +# credits: https://github.com/bogdanfinn/tls-client/blob/master/profiles/contributed_custom_profiles.go + +url = "https://tls.browserleaks.com/json" + +okhttp4_android10_ja3 = ",".join([ + "771", + "4865-4866-4867-49195-49196-52393-49199-49200-52392-49171-49172-156-157-47-53", + "0-23-65281-10-11-35-16-5-13-51-45-43-18-21", # FIXME: 18 should not be here. + "29-23-24", + "0" +]) + +okhttp4_android10_akamai = "4:16777216|16711681|0|m,p,a,s" + + +r = requests.get(url, ja3=okhttp4_android10_ja3, akamai=okhttp4_android10_akamai) +print(r.json()) diff --git a/tests/unittest/test_impersonate.py b/tests/unittest/test_impersonate.py index 7fd566fb..8a65c9a4 100644 --- a/tests/unittest/test_impersonate.py +++ b/tests/unittest/test_impersonate.py @@ -60,6 +60,7 @@ def test_customized_ja3_ciphers(): assert ciphers == "4865-4866-4867-49195-49199-49196-49200-52393-52392-49171" +# TODO change to parameterized test def test_customized_ja3_extensions(): url = "https://tls.browserleaks.com/json" ja3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,65037-65281-0-11-23-5-18-27-16-17513-10-35-43-45-13-51,25497-29-23-24,0" @@ -67,6 +68,17 @@ def test_customized_ja3_extensions(): _, _, extensions, _, _ = r["ja3_text"].split(",") assert extensions == "65037-65281-0-11-23-5-18-27-16-17513-10-35-43-45-13-51" + ja3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,65281-0-11-23-5-18-27-16-17513-10-35-43-45-13-51,25497-29-23-24,0" + r = requests.get(url, ja3=ja3).json() + _, _, extensions, _, _ = r["ja3_text"].split(",") + assert extensions == "65281-0-11-23-5-18-27-16-17513-10-35-43-45-13-51" + + # removed enable session_ticket() + ja3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,65281-0-11-23-5-18-27-16-17513-10-43-45-13-51,25497-29-23-24,0" + r = requests.get(url, ja3=ja3).json() + _, _, extensions, _, _ = r["ja3_text"].split(",") + assert extensions == "65281-0-11-23-5-18-27-16-17513-10-43-45-13-51" + def test_customized_ja3_curves(): url = "https://tls.browserleaks.com/json"