Search code examples
windowswinapiwinhttp

How to reset WinHTTP proxy credentials after a failed request?


I need to write code to download a file and the requirements are as follows. If the application is configured to use a proxy, attempt to download via the proxy. If that fails, attempt a direct connection. If no proxy is configured, attempt a direct connection. High-level pseudo-code:

if(ProxyEnabled)
    if(!DownloadWithProxy())
        DownloadWithoutProxy()
else
    DownloadWithoutProxy()

I'm using WinHTTP since this code will be running in a service. The actual downloading is straightforward but I'm having issues with the proxy settings. My current pseudo-code is as follows:

hSession = WinHttpOpen(..., WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, ...)
hConnect = WinHttpConnect(hSession, ...)
hRequest = WinHttpOpenRequest(hConnect, ...)
if(Proxy.Enabled)
{
    // set proxy server
    WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, ServerName)
    // set proxy credentials
    WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_PROXY, UserName, Password)
    if(!DownloadFile(hRequest))
    {
        // reset proxy server
        WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, NULL)
        // reset proxy credentials
        WinHttpSetCredentials(hRequest, WINHTTP_AUTH_TARGET_PROXY, NULL, NULL)
        DownloadFile(hRequest)
    }
}
else
{
    DownloadFile(hRequest)
}

Where DownloadFile() does the standard WinHttpSendRequest() and WinHttpReceiveResponse() sequence. Everything works fine except in the case where the download via the proxy fails. When that happens the call to WinHttpSetCredentials() to reset the credentials fails (with ERROR_INVALID_PARAMETER), and as a result the second call to DownloadFile() still tries to use the proxy (even though I reset it) and the credentials. NOTE: In this specific scenario I am using a valid proxy server but invalid proxy credentials to trigger the failure.

So I guess my question is what is the best approach? As far as I can tell it is not possible to reset the credentials set via WinHttpSetCredentials(), so I guess I should just recreate the request for each call to DownloadFile() rather than trying to be "clever" by reusing a single request object.


Solution

  • WinHttpSetCredentials() takes 6 parameters, one of which is the auth scheme. You have only shown 4 parameters, and do not say which auth scheme you are using. Overall, your sequence for authenticating is different than what MSDN suggests:

    Authentication in WinHTTP

    A typical WinHTTP application completes the following steps in order to handle authentication.

    •Request a resource with WinHttpOpenRequest and WinHttpSendRequest.
    •Check the response headers with WinHttpQueryHeaders.
    •If a 401 or 407 status code is returned indicating that authentication is required, call WinHttpQueryAuthSchemes to find an acceptable scheme.
    •Set the authentication scheme, username, and password with WinHttpSetCredentials.
    •Resend the request with the same request handle by calling WinHttpSendRequest.

    Also take note of this condition:

    The credentials set by WinHttpSetCredentials are only used for one request. WinHTTP does not cache the credentials to use in other requests, which means that applications must be written that can respond to multiple requests. If an authenticated connection is re-used, other requests may not be challenged, but your code should be able to respond to a request at any time.

    The documentation linked above includes the following code example for how to use WinHTTPSetCredentials() correctly:

    #include <windows.h>
    #include <winhttp.h>
    #include <stdio.h>
    
    #pragma comment(lib, "winhttp.lib")
    
    DWORD ChooseAuthScheme( DWORD dwSupportedSchemes )
    {
      //  It is the server's responsibility only to accept 
      //  authentication schemes that provide a sufficient
      //  level of security to protect the servers resources.
      //
      //  The client is also obligated only to use an authentication
      //  scheme that adequately protects its username and password.
      //
      //  Thus, this sample code does not use Basic authentication  
      //  becaus Basic authentication exposes the client's username
      //  and password to anyone monitoring the connection.
    
      if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NEGOTIATE )
        return WINHTTP_AUTH_SCHEME_NEGOTIATE;
      else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NTLM )
        return WINHTTP_AUTH_SCHEME_NTLM;
      else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_PASSPORT )
        return WINHTTP_AUTH_SCHEME_PASSPORT;
      else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_DIGEST )
        return WINHTTP_AUTH_SCHEME_DIGEST;
      else
        return 0;
    }
    
    struct SWinHttpSampleGet
    {
      LPCWSTR szServer;
      LPCWSTR szPath;
      BOOL fUseSSL;
      LPCWSTR szServerUsername;
      LPCWSTR szServerPassword;
      LPCWSTR szProxyUsername;
      LPCWSTR szProxyPassword;
    };
    
    void WinHttpAuthSample( IN SWinHttpSampleGet *pGetRequest )
    {
      DWORD dwStatusCode = 0;
      DWORD dwSupportedSchemes;
      DWORD dwFirstScheme;
      DWORD dwSelectedScheme;
      DWORD dwTarget;
      DWORD dwLastStatus = 0;
      DWORD dwSize = sizeof(DWORD);
      BOOL  bResults = FALSE;
      BOOL  bDone = FALSE;
    
      DWORD dwProxyAuthScheme = 0;
      HINTERNET  hSession = NULL, 
                 hConnect = NULL,
                 hRequest = NULL;
    
      // Use WinHttpOpen to obtain a session handle.
      hSession = WinHttpOpen( L"WinHTTP Example/1.0",  
                              WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                              WINHTTP_NO_PROXY_NAME, 
                              WINHTTP_NO_PROXY_BYPASS, 0 );
    
      INTERNET_PORT nPort = ( pGetRequest->fUseSSL ) ? 
                            INTERNET_DEFAULT_HTTPS_PORT  :
                            INTERNET_DEFAULT_HTTP_PORT;
    
      // Specify an HTTP server.
      if( hSession )
        hConnect = WinHttpConnect( hSession, 
                                   pGetRequest->szServer, 
                                   nPort, 0 );
    
      // Create an HTTP request handle.
      if( hConnect )
        hRequest = WinHttpOpenRequest( hConnect, 
                                       L"GET", 
                                       pGetRequest->szPath,
                                       NULL, 
                                       WINHTTP_NO_REFERER, 
                                       WINHTTP_DEFAULT_ACCEPT_TYPES,
                                       ( pGetRequest->fUseSSL ) ? 
                                           WINHTTP_FLAG_SECURE : 0 );
    
      // Continue to send a request until status code 
      // is not 401 or 407.
      if( hRequest == NULL )
        bDone = TRUE;
    
      while( !bDone )
      {
        //  If a proxy authentication challenge was responded to, reset
        //  those credentials before each SendRequest, because the proxy  
        //  may require re-authentication after responding to a 401 or  
        //  to a redirect. If you don't, you can get into a 
        //  407-401-407-401- loop.
        if( dwProxyAuthScheme != 0 )
          bResults = WinHttpSetCredentials( hRequest, 
                                            WINHTTP_AUTH_TARGET_PROXY, 
                                            dwProxyAuthScheme, 
                                            pGetRequest->szProxyUsername,
                                            pGetRequest->szProxyPassword,
                                            NULL );
        // Send a request.
        bResults = WinHttpSendRequest( hRequest,
                                       WINHTTP_NO_ADDITIONAL_HEADERS,
                                       0,
                                       WINHTTP_NO_REQUEST_DATA,
                                       0, 
                                       0, 
                                       0 );
    
        // End the request.
        if( bResults )
          bResults = WinHttpReceiveResponse( hRequest, NULL );
    
        // Resend the request in case of 
        // ERROR_WINHTTP_RESEND_REQUEST error.
        if( !bResults && GetLastError( ) == ERROR_WINHTTP_RESEND_REQUEST)
            continue;
    
        // Check the status code.
        if( bResults ) 
          bResults = WinHttpQueryHeaders( hRequest, 
                                          WINHTTP_QUERY_STATUS_CODE |
                                          WINHTTP_QUERY_FLAG_NUMBER,
                                          NULL, 
                                          &dwStatusCode, 
                                          &dwSize, 
                                          NULL );
    
        if( bResults )
        {
          switch( dwStatusCode )
          {
            case 200: 
              // The resource was successfully retrieved.
              // You can use WinHttpReadData to read the 
              // contents of the server's response.
              printf( "The resource was successfully retrieved.\n" );
              bDone = TRUE;
              break;
    
            case 401:
              // The server requires authentication.
              printf(" The server requires authentication. Sending credentials...\n" );
    
              // Obtain the supported and preferred schemes.
              bResults = WinHttpQueryAuthSchemes( hRequest, 
                                                  &dwSupportedSchemes, 
                                                  &dwFirstScheme, 
                                                  &dwTarget );
    
              // Set the credentials before resending the request.
              if( bResults )
              {
                dwSelectedScheme = ChooseAuthScheme( dwSupportedSchemes);
    
                if( dwSelectedScheme == 0 )
                  bDone = TRUE;
                else
                  bResults = WinHttpSetCredentials( hRequest, 
                                            dwTarget, 
                                            dwSelectedScheme,
                                            pGetRequest->szServerUsername,
                                            pGetRequest->szServerPassword,
                                            NULL );
              }
    
              // If the same credentials are requested twice, abort the
              // request.  For simplicity, this sample does not check
              // for a repeated sequence of status codes.
              if( dwLastStatus == 401 )
                bDone = TRUE;
    
              break;
    
            case 407:
              // The proxy requires authentication.
              printf( "The proxy requires authentication.  Sending credentials...\n" );
    
              // Obtain the supported and preferred schemes.
              bResults = WinHttpQueryAuthSchemes( hRequest, 
                                                  &dwSupportedSchemes, 
                                                  &dwFirstScheme, 
                                                  &dwTarget );
    
              // Set the credentials before resending the request.
              if( bResults )
                dwProxyAuthScheme = ChooseAuthScheme(dwSupportedSchemes);
    
              // If the same credentials are requested twice, abort the
              // request.  For simplicity, this sample does not check 
              // for a repeated sequence of status codes.
              if( dwLastStatus == 407 )
                bDone = TRUE;
              break;
    
            default:
              // The status code does not indicate success.
              printf("Error. Status code %d returned.\n", dwStatusCode);
              bDone = TRUE;
          }
        }
    
        // Keep track of the last status code.
        dwLastStatus = dwStatusCode;
    
        // If there are any errors, break out of the loop.
        if( !bResults ) 
            bDone = TRUE;
      }
    
      // Report any errors.
      if( !bResults )
      {
        DWORD dwLastError = GetLastError( );
        printf( "Error %d has occurred.\n", dwLastError );
      }
    
      // Close any open handles.
      if( hRequest ) WinHttpCloseHandle( hRequest );
      if( hConnect ) WinHttpCloseHandle( hConnect );
      if( hSession ) WinHttpCloseHandle( hSession );
    }
    

    You just have to inject your WinHttpSetOption(WINHTTP_OPTION_PROXY) where appropriate, and be sure that you are handling authentication requests for both proxy and non-proxy connections (in case the target HTTP server requires its own authentication).