From 10896fdbe20f01461165e1d230118758fec64054 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 16 Jan 2026 18:15:18 +0000 Subject: [PATCH 1/2] Enable automatic proxy logon with implicit credentials. Fixes #248 --- src/_native/bits.cpp | 26 +++++++++++++++++--------- src/_native/winhttp.cpp | 23 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/_native/bits.cpp b/src/_native/bits.cpp index 5bc7718..b3dd011 100644 --- a/src/_native/bits.cpp +++ b/src/_native/bits.cpp @@ -229,12 +229,17 @@ static HRESULT _job_setproxy(IBackgroundCopyJob *job) { } -static HRESULT _job_setcredentials(IBackgroundCopyJob *job, wchar_t *username, wchar_t *password) { +static HRESULT _job_setcredentials( + IBackgroundCopyJob *job, + BG_AUTH_TARGET target, + wchar_t *username, + wchar_t *password +) { IBackgroundCopyJob2 *job2 = NULL; HRESULT hr; BG_AUTH_CREDENTIALS creds = { - .Target = BG_AUTH_TARGET_SERVER, - .Scheme = BG_AUTH_SCHEME_BASIC, + .Target = target, + .Scheme = username ? BG_AUTH_SCHEME_BASIC : BG_AUTH_SCHEME_NEGOTIATE, .Credentials = { .Basic = { .UserName = username, @@ -243,10 +248,6 @@ static HRESULT _job_setcredentials(IBackgroundCopyJob *job, wchar_t *username, w } }; - if (!username && !password) { - return S_OK; - } - if (FAILED(hr = _inject_hr[3]) || FAILED(hr = job->QueryInterface(__uuidof(IBackgroundCopyJob2), (void **)&job2))) { return hr; @@ -285,7 +286,14 @@ PyObject *bits_begin(PyObject *, PyObject *args, PyObject *kwargs) { error_from_bits_hr(bcm, hr, "Setting proxy"); goto done; } - if ((username || password) && FAILED(hr = _job_setcredentials(job, username, password))) { + // Setting proxy credentials to NULL will automatically infer credentials + // if needed. It's a good default (provided users have not configured a + // malicious proxy server, which we can't do anything about here anyway). + if (FAILED(hr = _job_setcredentials(job, BG_AUTH_TARGET_PROXY, NULL, NULL))) { + error_from_bits_hr(bcm, hr, "Setting proxy credentials"); + goto done; + } + if (FAILED(hr = _job_setcredentials(job, BG_AUTH_TARGET_SERVER, username, password))) { error_from_bits_hr(bcm, hr, "Adding basic credentials to download job"); goto done; } @@ -387,7 +395,7 @@ PyObject *bits_retry_with_auth(PyObject *, PyObject *args, PyObject *kwargs) { HRESULT hr; PyObject *r = NULL; - if (FAILED(hr = _job_setcredentials(job, username, password))) { + if (FAILED(hr = _job_setcredentials(job, BG_AUTH_TARGET_SERVER, username, password))) { error_from_bits_hr(bcm, hr, "Adding basic credentials to download job"); goto done; } diff --git a/src/_native/winhttp.cpp b/src/_native/winhttp.cpp index d0323f4..9ca3293 100644 --- a/src/_native/winhttp.cpp +++ b/src/_native/winhttp.cpp @@ -190,6 +190,16 @@ static bool winhttp_apply_proxy(HINTERNET hSession, HINTERNET hRequest, const wc // Now resolve the proxy required for the specified URL CHECK_WINHTTP(WinHttpGetProxyForUrl(hSession, url, &proxy_opt, &proxy_info)); + // Enable proxy servers to automatically login with implicit credentials + // This is only used if the proxy sends a 407 response, otherwise, they are + // ignored. + CHECK_WINHTTP(WinHttpSetCredentials( + hRequest, + WINHTTP_AUTH_TARGET_PROXY, + WINHTTP_AUTH_SCHEME_NEGOTIATE, + NULL, NULL, NULL + )); + // Apply the proxy settings to the request CHECK_WINHTTP(WinHttpSetOption( hRequest, @@ -285,6 +295,17 @@ PyObject *winhttp_urlopen(PyObject *, PyObject *args, PyObject *kwargs) { 0 ); } + + // Allow proxies to automatically log in (we'll set the default credentials + // in winhttp_apply_proxy(), but this setting has to go on the session). + opt = WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW; + CHECK_WINHTTP(WinHttpSetOption( + hSession, + WINHTTP_OPTION_AUTOLOGON_POLICY, + &opt, + sizeof(opt) + )); + CHECK_WINHTTP(hSession); hConnection = WinHttpConnect( @@ -456,7 +477,7 @@ PyObject *winhttp_urlopen(PyObject *, PyObject *args, PyObject *kwargs) { PyObject *winhttp_isconnected(PyObject *, PyObject *, PyObject *) { INetworkListManager *nlm = NULL; VARIANT_BOOL connected; - + HRESULT hr = CoCreateInstance( CLSID_NetworkListManager, NULL, From b0ec8eb7d489b8a52d250f09a5fba2a05bc49e37 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 19 Jan 2026 13:58:52 +0000 Subject: [PATCH 2/2] Update test to account for always passing credentials now --- tests/test_urlutils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_urlutils.py b/tests/test_urlutils.py index e78769e..926bfe1 100644 --- a/tests/test_urlutils.py +++ b/tests/test_urlutils.py @@ -295,12 +295,15 @@ def test_bits_errors(localserver, tmp_path, inject_error): # Inject an error when adding credentials inject_error(0, 0, 0, 0xA0000001) - # No credentials specified, so does not raise - try: + # Implicit credentials are always specified + with pytest.raises(OSError) as ex: job = _native.bits_begin(conn, "PyManager Test", url, dest) - finally: - _native.bits_cancel(conn, job) - # Add credentials to cause injected error + # Original error is ours + assert ex.value.__context__.winerror & 0xFFFFFFFF == 0xA0000001 + # The final error is the missing message + assert ex.value.winerror & 0xFFFFFFFF == ERROR_MR_MID_NOT_FOUND + + # Add credentials also causes injected error with pytest.raises(OSError) as ex: job = _native.bits_begin(conn, "PyManager Test", url, dest, "x", "y") # Original error is ours