To bring the concepts of this chapter together, we build a simple HTTPS client. This client can connect to a given HTTPS web server and request the root document /.
Our program begins by including the needed chapter header, defining main(), and initializing Winsock as shown here:
/*https_simple.c*/
#include "chap09.h"
int main(int argc, char *argv[]) {
#if defined(_WIN32)
WSADATA d;
if (WSAStartup(MAKEWORD(2, 2), &d)) {
fprintf(stderr, "Failed to initialize.\n");
return 1;
}
#endif
We then initialize the OpenSSL library with the following code:
/*https_simple.c continued*/
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
The SSL_load_error_strings() function call is optional, but it's very useful if we run into problems.
We can also create an OpenSSL context. This is done by calling SSL_CTX_new() as shown by the following code:
/*https_simple.c continued*/
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) {
fprintf(stderr, "SSL_CTX_new() failed.\n");
return 1;
}
If we were going to do certificate verification, this would be a good place to include the SSL_CTX_load_verify_locations() function call, as explained in the Certificates section of this chapter. We're omitting certification verification in this example for simplicity, but it is important to include it in real-world applications.
Our program then checks that a hostname and a port number was passed in on the command line like so:
/*https_simple.c continued*/
if (argc < 3) {
fprintf(stderr, "usage: https_simple hostname port\n");
return 1;
}
char *hostname = argv[1];
char *port = argv[2];
The standard HTTPS port number is 443. Our program lets the user specify any port number, which can be useful for testing.
We then configure the remote address for the socket connection. This code uses the same technique that we've been using since Chapter 3, An In-Depth Overview of TCP connections. The code for this is as follows:
/*https_simple.c continued*/
printf("Configuring remote address...\n");
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *peer_address;
if (getaddrinfo(hostname, port, &hints, &peer_address)) {
fprintf(stderr, "getaddrinfo() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
printf("Remote address is: ");
char address_buffer[100];
char service_buffer[100];
getnameinfo(peer_address->ai_addr, peer_address->ai_addrlen,
address_buffer, sizeof(address_buffer),
service_buffer, sizeof(service_buffer),
NI_NUMERICHOST);
printf("%s %s\n", address_buffer, service_buffer);
We continue to create the socket using a call to socket(), and we connect it using the connect() function as follows:
/*https_simple.c continued*/
printf("Creating socket...\n");
SOCKET server;
server = socket(peer_address->ai_family,
peer_address->ai_socktype, peer_address->ai_protocol);
if (!ISVALIDSOCKET(server)) {
fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
printf("Connecting...\n");
if (connect(server,
peer_address->ai_addr, peer_address->ai_addrlen)) {
fprintf(stderr, "connect() failed. (%d)\n", GETSOCKETERRNO());
exit(1);
}
freeaddrinfo(peer_address);
printf("Connected.\n\n");
At this point, a TCP connection has been established. If we didn't need encryption, we could communicate over it directly. However, we are going to use OpenSSL to initiate a TLS/SSL connection over our TCP connection. The following code creates a new SSL object, sets the hostname for SNI, and initiates the TLS/SSL handshake:
/*https_simple.c continued*/
SSL *ssl = SSL_new(ctx);
if (!ctx) {
fprintf(stderr, "SSL_new() failed.\n");
return 1;
}
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
fprintf(stderr, "SSL_set_tlsext_host_name() failed.\n");
ERR_print_errors_fp(stderr);
return 1;
}
SSL_set_fd(ssl, server);
if (SSL_connect(ssl) == -1) {
fprintf(stderr, "SSL_connect() failed.\n");
ERR_print_errors_fp(stderr);
return 1;
}
The preceding code is explained in the Encrypted Sockets with OpenSSL section earlier in this chapter.
The call to SSL_set_tlsext_host_name() is optional, but useful if you may be connecting to a server that hosts multiple domains. Without this call, the server wouldn't know which certificates are relevant to this connection.
It is sometimes useful to know which cipher suite the client and server agreed upon. We can print the selected cipher suite with the following:
/*https_simple.c continued*/
printf("SSL/TLS using %s\n", SSL_get_cipher(ssl));
It is also useful to see the server's certificate. The following code prints the server's certificate:
/*https_simple.c continued*/
X509 *cert = SSL_get_peer_certificate(ssl);
if (!cert) {
fprintf(stderr, "SSL_get_peer_certificate() failed.\n");
return 1;
}
char *tmp;
if ((tmp = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0))) {
printf("subject: %s\n", tmp);
OPENSSL_free(tmp);
}
if ((tmp = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0))) {
printf("issuer: %s\n", tmp);
OPENSSL_free(tmp);
}
X509_free(cert);
The certificate subject should match the domain we're connecting to. The issuer should be a certificate authority that we trust. Note that the preceding code does not validate the certificate. Refer to the Certificates section in this chapter for more information.
We can then send our HTTPS request. This request is the same as if we were using plain HTTP. We begin by formatting the request into a buffer and then sending it over the encrypted connection using SSL_write(). The following code shows this:
/*https_simple.c continued*/
char buffer[2048];
sprintf(buffer, "GET / HTTP/1.1\r\n");
sprintf(buffer + strlen(buffer), "Host: %s:%s\r\n", hostname, port);
sprintf(buffer + strlen(buffer), "Connection: close\r\n");
sprintf(buffer + strlen(buffer), "User-Agent: https_simple\r\n");
sprintf(buffer + strlen(buffer), "\r\n");
SSL_write(ssl, buffer, strlen(buffer));
printf("Sent Headers:\n%s", buffer);
For more information about the HTTP protocol, please refer back to Chapter 6, Building a Simple Web Client.
Our client now simply waits for data from the server until the connection is closed. This is accomplished by using SSL_read() in a loop. The following code receives the HTTPS response:
/*https_simple.c continued*/
while(1) {
int bytes_received = SSL_read(ssl, buffer, sizeof(buffer));
if (bytes_received < 1) {
printf("\nConnection closed by peer.\n");
break;
}
printf("Received (%d bytes): '%.*s'\n",
bytes_received, bytes_received, buffer);
}
The preceding code also prints any data received over the HTTPS connection. Note that it does not parse the received HTTP data, which would be more complicated. See the https_get.c program in this chapter's code repository for a more advanced program that does parse the HTTP response.
Our simple client is almost done. We only have to shut down the TLS/SSL connection, close the socket, and clean up. This is done by the following code:
/*https_simple.c continued*/
printf("\nClosing socket...\n");
SSL_shutdown(ssl);
CLOSESOCKET(server);
SSL_free(ssl);
SSL_CTX_free(ctx);
#if defined(_WIN32)
WSACleanup();
#endif
printf("Finished.\n");
return 0;
}
That concludes https_simple.c.
You should be able to compile and run it on Windows using MinGW and the following commands:
gcc https_simple.c -o https_simple.exe -lssl -lcrypto -lws2_32
https_simple example.org 443
If you're using certain older versions of OpenSSL, you may also need an additional linker option—-lgdi32.
Compiling and executing on macOS and Linux can be done using the following:
gcc https_simple.c -o https_simple -lssl -lcrypto
./https_simple example.org 443
If you run into linker errors, you should check that your OpenSSL library is properly installed. You may find it helpful to attempt compiling the openssl_version.c program first.
The following screenshot shows successfully compiling https_simple.c and using it to connect to example.org:
The https_simple program should serve as an elementary example of the techniques of connecting as an HTTPS client. These same techniques can be applied to any TCP connection.
It's easy to apply these techniques to some of the programs developed earlier in the book, such as tcp_client and web_get.
It is also worth mentioning that, while TLS works only with TCP connections, Datagram Transport Layer Security (DTLS) aims to provide many of the same guarantees for User Datagram Protocol (UDP) datagrams. OpenSSL provides support for DTLS too.