fix(ip): percent-encode proxy credentials in the geoip lookup URL (#823)

Proxy.as_string() and ProxyHelper.asString() pasted the username and
password into scheme://user:pass@host:port unencoded. A password holding
`#`, `/` or `?` made the URL unparseable (requests: InvalidURL, impit:
"Failed to build Impit instance"), so public_ip() and proxy_exit_geo()
reported a working proxy as dead; a password holding `%41` parsed but was
decoded, and the gateway was sent `A` instead. Playwright gets the
credentials as separate fields, so only the lookup was affected.

Encode both with quote(safe='') / encodeURIComponent. Checked against a
local proxy recording Proxy-Authorization: requests and impit both decode
the encoded form back to the exact credentials.

Reported-by: alkaz-nodemaven (https://github.com/daijro/camoufox/issues/823)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-10-03 19:18:45 +00:00
co-authored by Claude Opus 5.5
parent ecf62d2362
commit e8989c4291
4 changed files with 38 additions and 4 deletions
+13
View File
@@ -11,6 +11,7 @@ timezone while its traffic went through the proxy:
import asyncio
from unittest import mock
from urllib.parse import unquote, urlsplit
import pytest
@@ -75,6 +76,18 @@ def test_lookup_goes_through_the_proxy_with_its_credentials(api, server, expecte
assert "203.0.113.7" in context.add_init_script.call_args.args[0]
@pytest.mark.parametrize("api", ["sync", "async"])
def test_credentials_with_url_delimiters_survive_the_proxy_url(api):
# A raw `#`, `/` or `?` stops the URL parsing, and a raw `%41` would be
# decoded into a different password (#823).
username, password = "us@r name", "p#ss/w?rd:%41"
with mock.patch.object(ip.requests, "get", return_value=_Response(EXIT)) as get:
_new_context(api, {"server": "proxy.example.com:8080", "username": username, "password": password})
url = urlsplit(get.call_args.kwargs["proxies"]["https"])
assert (url.hostname, url.port) == ("proxy.example.com", 8080)
assert (unquote(url.username), unquote(url.password)) == (username, password)
@pytest.mark.parametrize("api", ["sync", "async"])
@pytest.mark.parametrize(
"failure",