11from __future__ import annotations
22
33import json
4+ from dataclasses import dataclass
45from typing import Optional
5- from urllib .parse import unquote , urlsplit , urlunsplit
6+ from urllib .parse import quote , unquote , urlsplit , urlunsplit
7+
8+
9+ @dataclass (frozen = True )
10+ class _LegacyProxyParts :
11+ scheme : str
12+ host : str
13+ port : str
14+ username : str = ""
15+ password : str = ""
16+
17+
18+ def _parse_legacy_proxy (value : str ) -> _LegacyProxyParts | None :
19+ fields = value .split (":" )
20+ if len (fields ) == 2 and fields [1 ].isdigit ():
21+ return _LegacyProxyParts ("http" , fields [0 ], fields [1 ])
22+ if len (fields ) == 4 and fields [1 ].isdigit ():
23+ return _LegacyProxyParts ("http" , fields [0 ], fields [1 ], fields [2 ], fields [3 ])
24+ if len (fields ) >= 5 and fields [2 ].isdigit () and fields [0 ].lower () in {
25+ "http" ,
26+ "https" ,
27+ "socks4" ,
28+ "socks5" ,
29+ "socks5h" ,
30+ }:
31+ return _LegacyProxyParts (
32+ fields [0 ].lower (),
33+ fields [1 ],
34+ fields [2 ],
35+ fields [3 ],
36+ ":" .join (fields [4 :]),
37+ )
38+ return None
39+
40+
41+ def _legacy_to_url (parts : _LegacyProxyParts ) -> str :
42+ scheme = "socks5h" if parts .scheme == "socks5" else parts .scheme
43+ host = parts .host
44+ if ":" in host and not host .startswith ("[" ):
45+ host = f"[{ host } ]"
46+ auth = ""
47+ if parts .username or parts .password :
48+ username = quote (parts .username , safe = "" )
49+ password = quote (parts .password , safe = "" )
50+ auth = f"{ username } :{ password } @"
51+ return f"{ scheme } ://{ auth } { host } :{ parts .port } "
652
753
854def _is_auth_socks_proxy (scheme : str , username : str , password : str ) -> bool :
@@ -49,24 +95,65 @@ def normalize_proxy_url(proxy_url: Optional[str]) -> Optional[str]:
4995 if not value :
5096 return None
5197
98+ legacy = _parse_legacy_proxy (value )
99+ if legacy :
100+ return _legacy_to_url (legacy )
101+
52102 parts = urlsplit (value )
53103 if (parts .scheme or "" ).lower () == "socks5" :
54104 parts = parts ._replace (scheme = "socks5h" )
55105 return urlunsplit (parts )
56106 return value
57107
58108
109+ def redact_proxy_url (proxy_url : Optional [str ]) -> str :
110+ """Return a log-safe proxy URL without authentication credentials."""
111+ value = str (proxy_url or "" ).strip ()
112+ if not value :
113+ return ""
114+
115+ legacy = _parse_legacy_proxy (value )
116+ if legacy and "://" not in value :
117+ if legacy .username or legacy .password :
118+ if value .split (":" , 1 )[0 ].lower () in {
119+ "http" ,
120+ "https" ,
121+ "socks4" ,
122+ "socks5" ,
123+ "socks5h" ,
124+ }:
125+ return f"{ legacy .scheme } :{ legacy .host } :{ legacy .port } :***:***"
126+ return f"{ legacy .host } :{ legacy .port } :***:***"
127+ return f"{ legacy .host } :{ legacy .port } "
128+
129+ parts = urlsplit (value )
130+ if not parts .scheme :
131+ return "(configured proxy)"
132+
133+ host = parts .hostname or ""
134+ if ":" in host and not host .startswith ("[" ):
135+ host = f"[{ host } ]"
136+ try :
137+ port = f":{ parts .port } " if parts .port is not None else ""
138+ except ValueError :
139+ return f"{ parts .scheme } ://(configured proxy)"
140+ auth = (
141+ "***:***@" if parts .username is not None or parts .password is not None else ""
142+ )
143+ return urlunsplit (
144+ (parts .scheme , f"{ auth } { host } { port } " , parts .path , parts .query , parts .fragment )
145+ )
146+
147+
59148def build_requests_proxy_config (proxy_url : Optional [str ]) -> Optional [dict [str , str ]]:
60- if not proxy_url :
149+ normalized = normalize_proxy_url (proxy_url )
150+ if not normalized :
61151 return None
62- return {"http" : proxy_url , "https" : proxy_url }
152+ return {"http" : normalized , "https" : normalized }
63153
64154
65155def build_playwright_proxy_config (proxy_url : Optional [str ]) -> Optional [dict [str , str ]]:
66- if not proxy_url :
67- return None
68-
69- value = str (proxy_url ).strip ()
156+ value = normalize_proxy_url (proxy_url )
70157 if not value :
71158 return None
72159 parts = urlsplit (value )
@@ -77,8 +164,6 @@ def build_playwright_proxy_config(proxy_url: Optional[str]) -> Optional[dict[str
77164 return {"server" : server }
78165
79166 scheme = (parts .scheme or "" ).lower ()
80- if _is_auth_socks_proxy (scheme , parts .username or "" , parts .password or "" ):
81- return None
82167 if scheme == "socks5h" :
83168 scheme = "socks5"
84169
0 commit comments