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.
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.
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);