Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions tls_client/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,53 @@
from .__version__ import __version__

from typing import Any, Dict, List, Optional, Union
from json import dumps, loads
from json import dumps, loads, load
import urllib.parse
import urllib.request
import urllib.error
import base64
import ctypes
import uuid

_CHROME_STABLE_WIN_API = "https://versionhistory.googleapis.com/v1/chrome/platforms/win/channels/stable/versions"
_CHROME_LATEST_CACHE = None

def _get_latest_chrome_version(timeout: float = 5.0):
"""
Returns the latest stable version of Chrome for Windows (e.g., '142.0.7444.60').
Caches the result in memory to avoid multiple requests.
"""
global _CHROME_LATEST_CACHE
if _CHROME_LATEST_CACHE:
return _CHROME_LATEST_CACHE
try:
with urllib.request.urlopen(_CHROME_STABLE_WIN_API, timeout=timeout) as resp:
data = load(resp)
versions = data.get("versions", [])
if not versions:
return None

# The API usually brings the latest one first.
latest = versions[0].get("version")
if latest:
_CHROME_LATEST_CACHE = latest
return latest

# Backup: sort by numeric tuple major.minor.build.patch
def parse_version(vobj: dict):
v = vobj.get("version", "")
try:
return tuple(int(p) for p in v.split("."))
except ValueError:
return tuple()

versions_sorted = sorted(versions, key=parse_version, reverse=True)
latest = versions_sorted[0].get("version") if versions_sorted else None
_CHROME_LATEST_CACHE = latest
return latest
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError):
return None


class Session:

Expand Down Expand Up @@ -83,6 +124,15 @@ def __init__(
# iPadOS --> safari_ios_15_6
#
# for all possible client identifiers, check out the settings.py
if client_identifier == "chrome_latest":
latest_version = _get_latest_chrome_version()
if latest_version:
# We convert to 'chrome_<major>' which is the format expected by the fingerprint
major = latest_version.split(".")[0]
client_identifier = f"chrome_{major}"
# Optional: Keep the full version in case you need it later
self.chrome_full_version = latest_version

self.client_identifier = client_identifier

# Set JA3 --> TLSVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats
Expand Down Expand Up @@ -361,8 +411,15 @@ def execute_request(
# turn cookie jar into dict
# in the cookie value the " gets removed, because the fhttp library in golang doesn't accept the character
request_cookies = [
{'domain': c.domain, 'expires': c.expires, 'name': c.name, 'path': c.path, 'value': c.value.replace('"', "")}
{
'domain': getattr(c, "domain", ""),
'expires': getattr(c, "expires", ""),
'name': getattr(c, "name", ""),
'path': getattr(c, "path", ""),
'value': (getattr(c, "value", "") or "").replace('"', ""),
}
for c in cookies
if c is not None
]

# --- Proxy ----------------------------------------------------------------------------------------------------
Expand All @@ -371,7 +428,35 @@ def execute_request(
if type(proxy) is dict and "http" in proxy:
proxy = proxy["http"]
elif type(proxy) is str:
proxy = proxy
try:
if proxy.startswith("http"):
proxy = proxy
else:
if not "@" in proxy:
if len(proxy.split(":")) > 2:
ip, port, username, password = proxy.split(":")
proxy = f"http://{username}:{password}@{ip}:{port}"
else:
proxy = f"http://{proxy}"
else:
proxy = f"http://{proxy}"
except ValueError:
raise ValueError(
f"Invalid proxy format: {proxy}\n\n"
"Accepted proxy formats:\n"
" 1) http://ip:port\n"
" 2) https://ip:port\n"
" 3) http://username:password@ip:port\n"
" 4) https://username:password@ip:port\n"
" 5) ip:port (auto-converted to http://ip:port)\n"
" 6) ip:port:username:password (auto-converted to http://username:password@ip:port)\n"
" 7) username:password@ip:port (auto-converted to http://username:password@ip:port)\n"
" 8) {\"http\": \"<proxy_string>\"} (dictionary form — value is taken as-is)\n\n"
"Notes:\n"
" - Only HTTP/HTTPS proxy schemas are automatically recognized.\n"
" - SOCKS proxies must be passed via dict form (e.g. {\"http\": \"socks5://...\"}).\n"
" - IPv6 and credentials containing ':' or '@' are not supported by this parser.\n"
)
else:
proxy = ""

Expand Down
3 changes: 2 additions & 1 deletion tls_client/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"chrome_116_PSK_PQ",
"chrome_117",
"chrome_120",
"chrome_latest", # This retrieves the last stable version of Chrome in Windows using Google API.
# Safari
"safari_15_6_1",
"safari_16_0",
Expand Down Expand Up @@ -61,4 +62,4 @@
"confirmed_ios",
"confirmed_android",
"confirmed_android_2",
]
]