Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
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:
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 |