gemini-business2api-github / core /proxy_utils.py
lijunke
deploy: clean start with hf metadata
18081cf
"""
代理设置工具函数
支持格式:
- http://127.0.0.1:7890
- http://user:pass@127.0.0.1:7890
- socks5h://127.0.0.1:7890
- socks5h://user:pass@127.0.0.1:7890 | no_proxy=localhost,127.0.0.1,.local
NO_PROXY 格式:
- 逗号分隔的主机名或域名后缀
- 支持通配符前缀,如 .local 匹配 *.local
"""
import re
from typing import Tuple, Callable, Any, Optional
from urllib.parse import urlparse
import functools
def parse_proxy_setting(proxy_str: str) -> Tuple[str, str]:
"""
解析代理设置字符串,提取代理 URL 和 NO_PROXY 列表
Args:
proxy_str: 代理设置字符串,格式如 "http://127.0.0.1:7890 | no_proxy=localhost,127.0.0.1"
Returns:
Tuple[str, str]: (proxy_url, no_proxy_list)
- proxy_url: 代理地址,如 "http://127.0.0.1:7890"
- no_proxy_list: NO_PROXY 列表字符串,如 "localhost,127.0.0.1"
"""
if not proxy_str:
return "", ""
proxy_str = proxy_str.strip()
if not proxy_str:
return "", ""
# 检查是否包含 no_proxy 设置
# 支持格式: proxy_url | no_proxy=host1,host2
no_proxy = ""
proxy_url = proxy_str
# 使用 | 分隔代理和 no_proxy
if "|" in proxy_str:
parts = proxy_str.split("|", 1)
proxy_url = parts[0].strip()
no_proxy_part = parts[1].strip()
# 解析 no_proxy=xxx 格式
no_proxy_match = re.match(r"no_proxy\s*=\s*(.+)", no_proxy_part, re.IGNORECASE)
if no_proxy_match:
no_proxy = no_proxy_match.group(1).strip()
return normalize_proxy_url(proxy_url), no_proxy
def extract_host(url: str) -> str:
"""
从 URL 中提取主机名
Args:
url: 完整 URL,如 "https://mail.chatgpt.org.uk/api/emails"
Returns:
str: 主机名,如 "mail.chatgpt.org.uk"
"""
if not url:
return ""
url = url.strip()
if not url:
return ""
# 如果没有协议前缀,添加一个以便解析
if not url.startswith(("http://", "https://", "socks5://", "socks5h://")):
url = "http://" + url
try:
parsed = urlparse(url)
return parsed.hostname or ""
except Exception:
return ""
def no_proxy_matches(host: str, no_proxy: str) -> bool:
"""
检查主机是否在 NO_PROXY 豁免列表中
Args:
host: 要检查的主机名,如 "mail.chatgpt.org.uk"
no_proxy: NO_PROXY 列表字符串,如 "localhost,127.0.0.1,.local"
Returns:
bool: 如果主机在豁免列表中返回 True,否则返回 False
匹配规则:
- 精确匹配: "localhost" 匹配 "localhost"
- 域名后缀匹配: ".local" 匹配 "foo.local", "bar.foo.local"
- IP 地址匹配: "127.0.0.1" 精确匹配
"""
if not host or not no_proxy:
return False
host = host.lower().strip()
if not host:
return False
# 解析 no_proxy 列表
no_proxy_list = [item.strip().lower() for item in no_proxy.split(",") if item.strip()]
for pattern in no_proxy_list:
if not pattern:
continue
# 精确匹配
if host == pattern:
return True
# 域名后缀匹配 (如 .local 匹配 foo.local)
if pattern.startswith("."):
if host.endswith(pattern) or host == pattern[1:]:
return True
else:
# 也支持不带点的后缀匹配 (如 local 匹配 foo.local)
if host.endswith("." + pattern):
return True
return False
def normalize_proxy_url(proxy_str: str) -> str:
"""
标准化代理 URL 格式
支持的输入格式:
- http://127.0.0.1:7890
- http://user:pass@127.0.0.1:7890
- socks5://127.0.0.1:7890
- socks5h://127.0.0.1:7890
- 127.0.0.1:7890 (自动添加 http://)
- host:port:user:pass (旧格式,自动转换)
Returns:
str: 标准化的代理 URL
"""
if not proxy_str:
return ""
proxy_str = proxy_str.strip()
if not proxy_str:
return ""
# 如果已经是标准 URL 格式,直接返回
if proxy_str.startswith(("http://", "https://", "socks5://", "socks5h://")):
return proxy_str
# 尝试解析旧格式 host:port:user:pass
parts = proxy_str.split(":")
if len(parts) == 4:
host, port, user, password = parts
return f"http://{user}:{password}@{host}:{port}"
elif len(parts) == 2:
# host:port 格式
return f"http://{proxy_str}"
# 无法识别的格式,尝试添加 http:// 前缀
return f"http://{proxy_str}"
def request_with_proxy_fallback(request_func: Callable, *args, **kwargs) -> Any:
"""
带代理失败回退的请求包装器
如果代理连接失败,自动禁用代理重试一次
Args:
request_func: 原始请求函数
*args, **kwargs: 传递给请求函数的参数
Returns:
请求响应对象
Raises:
原始异常(如果直连也失败)
"""
# 代理相关的错误类型
PROXY_ERRORS = (
"ProxyError",
"ConnectTimeout",
"ConnectionError",
"407", # Proxy Authentication Required
"502", # Bad Gateway (代理问题)
"503", # Service Unavailable (代理问题)
)
try:
# 首次尝试(使用代理)
return request_func(*args, **kwargs)
except Exception as e:
error_str = str(e)
error_type = type(e).__name__
# 检查是否是代理相关错误
is_proxy_error = any(err in error_str or err in error_type for err in PROXY_ERRORS)
if is_proxy_error and "proxies" in kwargs:
# 禁用代理重试
original_proxies = kwargs.get("proxies")
kwargs["proxies"] = None
try:
# 直连重试
return request_func(*args, **kwargs)
except Exception:
# 直连也失败,恢复原始代理设置并抛出原始异常
kwargs["proxies"] = original_proxies
raise e
else:
# 不是代理错误,直接抛出
raise