[ Team LiB ] Previous Section Next Section

9.4 Securing Web Communication on Windows Using the WinInet API

9.4.1 Problem

You are developing a Windows program that needs to connect to an HTTP server with SSL enabled. You want to use the Microsoft WinInet API to communicate with the HTTP server.

9.4.2 Solution

The Microsoft WinInet API was introduced with Internet Explorer 3.0. It provides a set of functions that allow programs easy access to FTP, Gopher, HTTP, and HTTPS servers. For HTTPS servers, the details of using SSL are hidden from the programmer, allowing the programmer to concentrate on the data that needs to be exchanged, rather than protocol details.

9.4.3 Discussion

The Microsoft WinInet API is a rich API that makes client-side interaction with FTP, Gopher, HTTP, and HTTPS servers easy; as with most Windows APIs, however, a sizable amount of code is still required. Because of the wealth of options available, we won't provide fully working code for a WinInet API wrapper here. Instead, we'll discuss the API and provide code samples for the parts of the API that are interesting from a security standpoint. We encourage you to consult Microsoft's documentation on the API to learn about all that the API can do.

If you're going to establish a connection to a web server using SSL with WinInet, the first thing you need to do is create an Internet session by calling InternetOpen( ). This function initializes and returns an object handle that is needed to actually establish a connection. It takes care of such details as presenting the user with the dial-in UI if the user is not connected to the Internet and the system is so configured. Although any number of calls may be made to InternetOpen( ) by a single application, it generally needs to be called only once. The handle it returns can be reused any number of times.

#include <windows.h>
#include <wininet.h>
   
HINTERNET hInternetSession;
LPSTR     lpszAgent       = "Secure Programming Cookbook Recipe 9.4";
DWORD     dwAccessType    = INTERNET_OPEN_TYPE_PROXY;
LPSTR     lpszProxyName   = 0;
LPSTR     lpszProxyBypass = 0;
DWORD     dwFlags         = 0;
   
hInternetSession = InternetOpen(lpszAgent, dwAccessType, lpszProxyName,
                                lpszProxyBypass, dwFlags);

If you set dwAccessType to INTERNET_OPEN_TYPE_PROXY, lpszProxyName to 0, and lpszProxyBypass to 0, the system defaults for HTTP access are used. If the system is configured to use a proxy, it will be used as required. The lpszAgent argument is passed to servers as the client's HTTP agent string. It may be set as any custom string, or it may be set to the same string a specific browser might send to a web server when making a request.

The next step is to connect to the server. You do this by calling InternetConnect( ), which will return a new handle to an object that stores all of the relevant connection information. The two obvious requirements for this function are the name of the server to connect to and the port on which to connect. The name of the server may be specified as either a hostname or a dotted-decimal IP address. You can specify the port as a number or use the constant INTERNET_DEFAULT_HTTPS_PORT to connect to the default SSL-enabled HTTP port 443.

HINTERNET     hConnection;
LPSTR         lpszServerName = "www.amazon.com";
INTERNET_PORT nServerPort    = INTERNET_DEFAULT_HTTPS_PORT;
LPSTR         lpszUsername   = 0;
LPSTR         lpszPassword   = 0;
DWORD         dwService      = INTERNET_SERVICE_HTTP;
DWORD         dwFlags        = 0;
DWORD         dwContext      = 0;
   
hConnection = InternetConnect(hInternetSession, lpszServerName, nServerPort,
                              lpszUsername, lpszPassword, dwService, dwFlags,
                              dwContext);

The call to InternetConnect( ) actually establishes a connection to the remote server. If the connection attempt fails for some reason, the return value is NULL, and the error code can be retrieved via GetLastError( ). Otherwise, the new object handle is returned. If multiple requests to the same server are necessary, you should use the same handle, to avoid the overhead of establishing multiple connections.

Once a connection to the server has been established, a request object must be constructed. This object is a container for various information: the resource that will be requested, the headers that will be sent, a set of flags that dictate how the request is to behave, header information returned by the server after the request has been submitted, and other information. A new request object is constructed by calling HttpOpenRequest( ).

HINTERNET hRequest;
LPSTR     lpszVerb        = "GET";
LPSTR     lpszObjectName  = "/";
LPSTR     lpszVersion     = "HTTP/1.1";
LPSTR     lpszReferer     = 0;
LPSTR     lpszAcceptTypes = 0;
DWORD     dwFlags         = INTERNET_FLAG_SECURE |
                            INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP |
                            INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS;
DWORD     dwContext       = 0;
   
hRequest = HttpOpenRequest(hConnection, lpszVerb, lpszObjectName, lpszVersion,
                           lpszReferer, lpszAcceptTypes, dwFlags, dwContext);

The lpszVerb argument controls the type of request that will be made, which can be any valid HTTP request, such as GET or POST. The lpszObjectName argument is the resource that is to be requested, which is normally the part of a URL that follows the server name, starting with the forward slash and ending before the query string (which starts with a question mark). Specifying lpszAcceptTypes as 0 tells the server that we can accept any kind of text document; it is equivalent to a MIME type of "text/*".

The most interesting argument passed to HttpOpenRequest( ) is dwFlags. A large number of flags are defined, but only five deal specifically with HTTP over SSL:

INTERNET_FLAG_IGNORE_CERT_CN_INVALID

Normally, as part of verification of the server's certificate, WinInet will verify that the hostname is contained in the certificate's commonName field or subjectAltName extension. If this flag is specified, the hostname check will not be performed. (See Recipe 10.4 and Recipe 10.8 for discussions of the importance of performing hostname checks on certificates.)

INTERNET_FLAG_IGNORE_CERT_DATE_INVALID

An important part of verifying the validity of an X.509 certificate involves checking the dates for which a certificate is valid. If the current date is outside the certificate's valid date range, the certificate should be considered invalid. If this flag is specified, the certificate's validity dates are not checked. This option should never be used in a released version of a product.

INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP

If this flag is specified and the server attempts to redirect the client to a non-SSL URL, the redirection will be ignored. You should always include this flag so you can be sure you are not transferring in the clear data that you expect to be protected.

INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS

If this flag is specified and the server attempts to redirect the client to an SSL- protected URL, the redirection will be ignored. If you're expecting to be communicating only with servers under your own control, it's safe to omit this flag; if not, you might want to consider including it so you're not transferred somewhere other than expected.

INTERNET_FLAG_SECURE

This is the all-important flag. When this flag is included, the use of SSL on the connection is enabled. Without it, SSL is not used, and all data is transferred in the clear. Obviously, you want to include this flag.

Once the request object has been constructed, the request needs to be sent to the server. This is done by calling HttpSendRequest( ) with the request object. Additional headers can be included with the request submission, as well as any optional data to be sent after the headers. You will want to send optional data when performing a POST operation. Additional headers and optional data are both specified as strings and the lengths of the strings.

BOOL  bResult;
LPSTR lpszHeaders      = 0;
DWORD dwHeadersLength  = 0;
LPSTR lpszOptional     = 0;
DWORD dwOptionalLength = 0;
   
bResult = HttpSendRequest(hRequest, lpszHeaders, dwHeadersLength, lpOptional,
                          dwOptionalLength);

After sending the request, the server's response can be retrieved. As part of sending the request, WinInet will retrieve the response headers from the server. Information about the response can be obtained using the HttpQueryInfo( ) function. A complete list of the information that may be available can be found in the WinInet documentation, but for our purposes, the only information we're concerned with is the content length. The server is not required to send a content length header back as part of its response, so we must also be able to handle the case where it is not sent. Response data sent by the server after its response headers can be obtained by calling InternetReadFile( ) as many times as necessary to retrieve all of the data.

DWORD  dwContentLength, dwIndex, dwInfoLevel;
DWORD  dwBufferLength, dwNumberOfBytesRead, dwNumberOfBytesToRead;
LPVOID lpBuffer, lpFullBuffer, lpvBuffer;
   
dwInfoLevel    = HTTP_QUERY_CONTENT_LENGTH;
lpvBuffer      = (LPVOID)&dwContentLength;
dwBufferLength = sizeof(dwContentLength);
dwIndex        = 0;
HttpQueryInfo(hRequest, dwInfoLevel, lpvBuffer, &dwBufferLength, &dwIndex);
if (dwIndex != ERROR_HTTP_HEADER_NOT_FOUND) {
  /* Content length is known.  Read only that much data. */
  lpBuffer = GlobalAlloc(GMEM_FIXED, dwContentLength);
  InternetReadFile(hRequest, lpBuffer, dwContentLength, &dwNumberOfBytesRead);
} else {
  /* Content length is not known.  Read until EOF is reached. */
  dwContentLength = 0;
  dwNumberOfBytesToRead = 4096;
  lpFullBuffer = lpBuffer = GlobalAlloc(GMEM_FIXED, dwNumberOfBytesToRead);
  while (InternetReadFile(hRequest, lpBuffer, dwNumberOfBytesToRead,
                         &dwNumberOfBytesRead)) {
    dwContentLength += dwNumberOfBytesRead;
    if (dwNumberOfBytesRead != dwNumberOfBytesToRead) break;
    lpFullBuffer = GlobalReAlloc(lpFullBuffer, dwContentLength + 
                                 dwNumberOfBytesToRead, 0);
    lpBuffer = (LPVOID)((LPBYTE)lpFullBuffer + dwContentLength);
  }
  lpFullBuffer = lpBuffer = GlobalReAlloc(lpFullBuffer, dwContentLength, 0);
}

After the data has been read with InternetReadFile( ), the variable lpBuffer will hold the contents of the server's response, and the variable dwContentLength will hold the number of bytes contained in the response data buffer. At this point, the request has been completed, and the request object should be destroyed by calling InternetCloseHandle( ). If additional requests to the same connection are required, a new request object can be created and used with the same connection handle from the call to InternetConnect( ). When no more requests are to be made on the same connection, InternetCloseHandle( ) should be used to close the connection. Finally, when no more WinInet activity is to take place using the Internet session object created by InternetConnect( ), InternetCloseHandle( ) should be called to clean up that object as well.

InternetCloseHandle(hRequest);
InternetCloseHandle(hConnection);
InternetCloseHandle(hInternetSession);

9.4.4 See Also

Recipe 10.4, Recipe 10.8

    [ Team LiB ] Previous Section Next Section