From 72c5342a6badeb89dae8fee9523d8929d951ad26 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 29 Jun 2026 21:34:36 -0400 Subject: [PATCH] fix: AP-1 percent-encode url() creds, AP-2 reject next(session=) collision AP-1: url()/aiohttp() percent-encode user/password so reserved chars (/ # ? @) produce a valid url, mirroring the parse-side unquote. AP-2: next(session=...) raises a clear ValueError instead of an opaque str.format TypeError. net.current_ip uses json(content_type=None) + dict guard. Signed-off-by: disqualifier --- src/aioproxies/manager.py | 4 ++++ src/aioproxies/net.py | 6 ++++-- src/aioproxies/proxy.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/aioproxies/manager.py b/src/aioproxies/manager.py index 7c5b012..9103291 100644 --- a/src/aioproxies/manager.py +++ b/src/aioproxies/manager.py @@ -125,6 +125,10 @@ class AioProxies: if self._static is not None: return self._static if self.template is not None: + if "session" in fields: + # `session` is auto-filled with a fresh id; a caller-supplied one would + # collide in str.format with an opaque TypeError — reject it clearly + raise ValueError("'session' is filled automatically; do not pass it to next()") try: filled = self.template.format(session=self.session_id(), **fields) except (KeyError, IndexError) as exc: diff --git a/src/aioproxies/net.py b/src/aioproxies/net.py index 3da4bc8..195dc3a 100644 --- a/src/aioproxies/net.py +++ b/src/aioproxies/net.py @@ -37,8 +37,10 @@ async def current_ip( try: async with aiohttp.ClientSession(timeout=t) as session: async with session.get(test_url, proxy=p.url()) as resp: - data = await resp.json() - return data.get("ip") + # content_type=None: an echo endpoint may return text/plain json; the + # default would raise ContentTypeError and silently return None + data = await resp.json(content_type=None) + return data.get("ip") if isinstance(data, dict) else None except Exception as exc: log.warning("ip check failed: %s", exc) return None diff --git a/src/aioproxies/proxy.py b/src/aioproxies/proxy.py index a9ffd13..d9d17cf 100644 --- a/src/aioproxies/proxy.py +++ b/src/aioproxies/proxy.py @@ -13,7 +13,7 @@ input shape. `canonical_key()` extends that to dict/url forms. """ from dataclasses import dataclass from typing import Dict, Optional, Union -from urllib.parse import unquote, urlsplit +from urllib.parse import quote, unquote, urlsplit SCHEME_HTTP = "http" SCHEME_SOCKS5 = "socks5" @@ -48,9 +48,15 @@ class Proxy: return f"{host}:{self.port}" def url(self, scheme: str = SCHEME_HTTP) -> str: - """render as a url, embedding auth when present""" + """render as a url, embedding auth when present + + credentials are percent-encoded so reserved chars (/ # ? @ :) in a user or + password produce a valid url; this mirrors the unquote() on the parse side. + """ if self.has_auth: - return f"{scheme}://{self.user}:{self.password}@{self.host}:{self.port}" + user = quote(str(self.user), safe="") + password = quote(str(self.password), safe="") + return f"{scheme}://{user}:{password}@{self.host}:{self.port}" return f"{scheme}://{self.host}:{self.port}" def aiohttp(self) -> Dict[str, str]: