199 lines
7.2 KiB
Python
199 lines
7.2 KiB
Python
from typing import Dict, Optional, Union
|
||
from pathlib import Path
|
||
from urllib.parse import quote
|
||
import os
|
||
|
||
|
||
def _parse_proxy_line(line: str) -> Optional[str]:
|
||
# Формат: scheme:ip:port[:login[:pass]]
|
||
# Примеры:
|
||
# socks5:127.0.0.1:9050
|
||
# socks5:127.0.0.1:9050:user:pass
|
||
# http:127.0.0.1:8888
|
||
parts = [p.strip() for p in line.strip().split(":")]
|
||
if len(parts) < 3:
|
||
return None
|
||
scheme, host, port = parts[0], parts[1], parts[2]
|
||
user = parts[3] if len(parts) >= 4 and parts[3] else None
|
||
password = parts[4] if len(parts) >= 5 and parts[4] else None
|
||
auth = ""
|
||
if user:
|
||
auth = quote(user)
|
||
if password:
|
||
auth += f":{quote(password)}"
|
||
auth += "@"
|
||
# Исправление для socks5: httpx ожидает схему socks5:// (не socks://)
|
||
if scheme == "socks":
|
||
scheme = "socks5"
|
||
# Явно проверяем протокол, чтобы был http://, https:// или socks5://
|
||
if not scheme.startswith(("http", "socks")):
|
||
scheme = "http"
|
||
return f"{scheme}://{auth}{host}:{port}"
|
||
|
||
|
||
def _read_proxy_from_file() -> Optional[str]:
|
||
file_path = Path("proxy.txt")
|
||
if not file_path.exists():
|
||
return None
|
||
try:
|
||
for raw in file_path.read_text(encoding="utf-8").splitlines():
|
||
line = raw.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
# поддержим дополнительные ключи вида key=value в этом же файле (разберём ниже)
|
||
if "=" in line:
|
||
continue
|
||
url = _parse_proxy_line(line)
|
||
if url:
|
||
return url
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
|
||
def build_httpx_proxies() -> Optional[Dict[str, str]]:
|
||
# Читаем только из proxy.txt (без переменных окружения)
|
||
url = _read_proxy_from_file()
|
||
if not url:
|
||
return None
|
||
# Для httpx корректнее указывать схемы явно
|
||
return {
|
||
"http://": url,
|
||
"https://": url,
|
||
}
|
||
|
||
|
||
def _read_kv_from_proxy_file() -> Dict[str, str]:
|
||
"""
|
||
Поддержка дополнительных опций в proxy.txt:
|
||
ca=/полный/путь/к/burp-ca.pem
|
||
verify=false # отключить проверку сертификатов (для отладки)
|
||
"""
|
||
out: Dict[str, str] = {}
|
||
p = Path("proxy.txt")
|
||
if not p.exists():
|
||
return out
|
||
try:
|
||
for raw in p.read_text(encoding="utf-8").splitlines():
|
||
line = raw.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
if "=" not in line:
|
||
continue
|
||
k, v = line.split("=", 1)
|
||
out[k.strip().lower()] = v.strip()
|
||
except Exception:
|
||
return out
|
||
return out
|
||
|
||
def _read_second_bare_flag_from_proxy() -> Optional[bool]:
|
||
"""
|
||
Читает «вторую голую строку» после URL в proxy.txt и интерпретирует как флаг verify:
|
||
true/1/yes/on -> True
|
||
false/0/no/off -> False
|
||
Возвращает None, если строка отсутствует или не распознана.
|
||
"""
|
||
try:
|
||
p = Path("proxy.txt")
|
||
if not p.exists():
|
||
return None
|
||
lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines()]
|
||
# найдём первую «URL» строку (без '=' и не пустую/коммент)
|
||
idx_url = -1
|
||
for i, ln in enumerate(lines):
|
||
if not ln or ln.startswith("#") or "=" in ln:
|
||
continue
|
||
idx_url = i
|
||
break
|
||
if idx_url >= 0:
|
||
# ищем следующую «голую» строку
|
||
for j in range(idx_url + 1, len(lines)):
|
||
ln = lines[j].strip()
|
||
if not ln or ln.startswith("#") or "=" in ln:
|
||
continue
|
||
low = ln.lower()
|
||
if low in ("1", "true", "yes", "on"):
|
||
return True
|
||
if low in ("0", "false", "no", "off"):
|
||
return False
|
||
# если это не похожее на флаг — считаем отсутствующим
|
||
break
|
||
except Exception:
|
||
return None
|
||
return None
|
||
def get_tls_verify() -> Union[bool, str]:
|
||
"""
|
||
Возвращает значение для параметра httpx.AsyncClient(verify=...):
|
||
- путь к PEM-бандлу (строка), если нашли ca=... или файл proxy-ca.pem в корне
|
||
- False, если verify=false/insecure=1/AGENTUI_VERIFY=false
|
||
- True по умолчанию
|
||
- Новое: можно задать флаг второй «голой» строкой в proxy.txt (после URL прокси):
|
||
пример:
|
||
http:127.0.0.1:8888
|
||
false
|
||
или
|
||
http:127.0.0.1:8888
|
||
true
|
||
"""
|
||
# 1) Переменные окружения имеют приоритет
|
||
env_verify = os.getenv("AGENTUI_VERIFY")
|
||
if env_verify is not None and env_verify.strip().lower() in ("0", "false", "no", "off"):
|
||
return False
|
||
env_ca = os.getenv("AGENTUI_CA")
|
||
if env_ca:
|
||
path = Path(env_ca).expanduser()
|
||
if path.exists():
|
||
return str(path)
|
||
|
||
# 2) proxy.txt ключи
|
||
kv = _read_kv_from_proxy_file()
|
||
if kv.get("verify", "").lower() in ("0", "false", "no", "off"):
|
||
return False
|
||
if "ca" in kv:
|
||
path = Path(kv["ca"]).expanduser()
|
||
if path.exists():
|
||
return str(path)
|
||
# 2.1) Дополнительно: поддержка второй строки без ключа — true/false
|
||
second = _read_second_bare_flag_from_proxy()
|
||
if second is True:
|
||
return True
|
||
if second is False:
|
||
return False
|
||
|
||
# 3) Файл по умолчанию в корне проекта
|
||
default_ca = Path("proxy-ca.pem")
|
||
if default_ca.exists():
|
||
return str(default_ca)
|
||
|
||
# 4) По умолчанию строгая проверка
|
||
return True
|
||
|
||
|
||
def is_verify_explicit() -> bool:
|
||
"""
|
||
Возвращает True, если пользователь ЯВНО задал политику проверки TLS,
|
||
чтобы клиент не переопределял её значением по умолчанию.
|
||
Учитываются:
|
||
- переменные окружения: AGENTUI_VERIFY, AGENTUI_CA
|
||
- ключи в proxy.txt: verify=..., ca=...
|
||
- файл proxy-ca.pem в корне проекта
|
||
- Новое: «вторая голая строка» после URL в proxy.txt со значением true/false
|
||
"""
|
||
if os.getenv("AGENTUI_VERIFY") is not None:
|
||
return True
|
||
if os.getenv("AGENTUI_CA"):
|
||
return True
|
||
|
||
kv = _read_kv_from_proxy_file()
|
||
if "verify" in kv or "ca" in kv:
|
||
return True
|
||
# Вторая «голая» строка как явный флаг
|
||
second = _read_second_bare_flag_from_proxy()
|
||
if second is not None:
|
||
return True
|
||
|
||
if Path("proxy-ca.pem").exists():
|
||
return True
|
||
|
||
return False
|