In this chapter, we develop a simple time server that displays the time to an HTTPS client. This program is an adaptation of time_server.c from Chapter 2, Getting to Grips with Socket APIs, which served the time over plain HTTP. Our program begins by including the chapter header, defining main(), and initializing Winsock on Windows. The code for this is as follows:
/*tls_time_server.c*/
#include "chap10.h"
int main() {
#if defined(_WIN32)
WSADATA d;
if (WSAStartup(MAKEWORD(2, 2), &d)) {
fprintf(stderr, "Failed to initialize.\n");
return 1;
}
#endif
The OpenSSL library is then initialized with the following code:
/*tls_time_server.c continued*/
SSL_library_init();
OpenSSL_add_all_algorithms();
SSL_load_error_strings();
An SSL context object must be created for our server. This is done using a call to SSL_CTX_new(). The following code shows this call:
/*tls_time_server.c continued*/
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
if (!ctx) {
fprintf(stderr, "SSL_CTX_new() failed.\n");
return 1;
}
If you're using an older version of OpenSSL, you may need to replace TLS_server_method() with TLSv1_2_server_method() in the preceding code. However, you should probably upgrade to a newer OpenSSL version instead.
Once the SSL context has been created, we can associate our server's certificate with it. The following code sets the SSL context to use our certificate:
/*tls_time_server.c continued*/
if (!SSL_CTX_use_certificate_file(ctx, "cert.pem" , SSL_FILETYPE_PEM)
|| !SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM)) {
fprintf(stderr, "SSL_CTX_use_certificate_file() failed.\n");
ERR_print_errors_fp(stderr);
return 1;
}
Make sure that you've generated a proper certificate and key. Refer to the Self-signed certificate with OpenSSL section from earlier in this chapter.
Once the SSL context is configured with the proper certificate, our program creates a listening TCP socket in the normal way. It begins with a call to getaddrinfo() and socket(), as shown in the following code:
/*tls_time_server.c continued*/
printf("Configuring local address...\n");
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
struct addrinfo *bind_address;
getaddrinfo(0, "8080", &hints, &bind_address);
printf("Creating socket...\n");
SOCKET socket_listen;
socket_listen = socket(bind_address->ai_family,
bind_address->ai_socktype, bind_address->ai_protocol);
if (!ISVALIDSOCKET(socket_listen)) {
fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
return 1;
}
The socket created by the preceding code is bound to the listening address with bind(). The listen() function is used to set the socket in a listening state. The following code demonstrates this:
/*tls_time_server.c continued*/
printf("Binding socket to local address...\n");
if (bind(socket_listen,
bind_address->ai_addr, bind_address->ai_addrlen)) {
fprintf(stderr, "bind() failed. (%d)\n", GETSOCKETERRNO());
return 1;
}
freeaddrinfo(bind_address);
printf("Listening...\n");
if (listen(socket_listen, 10) < 0) {
fprintf(stderr, "listen() failed. (%d)\n", GETSOCKETERRNO());
return 1;
}
If the preceding code isn't familiar, please refer to Chapter 3, An In-Depth Overview of TCP Connections.
Note that the preceding code sets the listening port number to 8080. The standard port for HTTPS is 443. It is often more convenient to test with a high port number, since low port numbers require special privileges on some operating systems.
Our server uses a while loop to accept multiple connections. Note that this isn't true multiplexing, as only one connection is handled at a time. However, it proves convenient for testing purposes to be able to handle multiple connections serially. Our self-signed certificate causes mainstream browsers to reject our connection on the first try. The connection only succeeds after an exception is added. By having our code loop, it makes adding this exception easier.
Our while loop begins by using accept() to wait for new connections. This is done by the following code:
/*tls_time_server.c continued*/
while (1) {
printf("Waiting for connection...\n");
struct sockaddr_storage client_address;
socklen_t client_len = sizeof(client_address);
SOCKET socket_client = accept(socket_listen,
(struct sockaddr*) &client_address, &client_len);
if (!ISVALIDSOCKET(socket_client)) {
fprintf(stderr, "accept() failed. (%d)\n", GETSOCKETERRNO());
return 1;
}
Once the connection is accepted, we use getnameinfo() to print out the client's address. This is sometimes useful for debugging purposes. The following code does this:
/*tls_time_server.c continued*/
printf("Client is connected... ");
char address_buffer[100];
getnameinfo((struct sockaddr*)&client_address,
client_len, address_buffer, sizeof(address_buffer), 0, 0,
NI_NUMERICHOST);
printf("%s\n", address_buffer);
Once the TCP connection is established, an SSL object needs to be created. This is done with a call to SSL_new(), as shown by the following code:
/*tls_time_server.c continued*/
SSL *ssl = SSL_new(ctx);
if (!ctx) {
fprintf(stderr, "SSL_new() failed.\n");
return 1;
}
The SSL object is associated with the open socket by a call to SSL_set_fd(). Then, a TLS/SSL connection can be initialized with a call to SSL_accept(). The following code shows this:
/*tls_time_server.c continued*/
SSL_set_fd(ssl, socket_client);
if (SSL_accept(ssl) <= 0) {
fprintf(stderr, "SSL_accept() failed.\n");
ERR_print_errors_fp(stderr);
SSL_shutdown(ssl);
CLOSESOCKET(socket_client);
SSL_free(ssl);
continue;
}
printf ("SSL connection using %s\n", SSL_get_cipher(ssl));
In the preceding code, the call to the SSL_accept() function can fail for many reasons. For example, if the connected client doesn't trust our certificate, or the client and server can't agree on a cipher suite, then the call to SSL_accept() fails. When it fails, we just clean up the allocated resources and use continue to repeat our listening loop.
Once the TCP and TLS/SSL connections are fully open, we use SSL_read() to receive the client's request. Our program ignores the content of this request. This is because our program only serves the time. It doesn't matter what the client has asked for—our server responds with the time.
The following code uses SSL_read() to wait on and read the client's request:
/*tls_time_server.c continued*/
printf("Reading request...\n");
char request[1024];
int bytes_received = SSL_read(ssl, request, 1024);
printf("Received %d bytes.\n", bytes_received);
The following code uses SSL_write() to transmit the HTTP headers to the client:
/*tls_time_server.c continued*/
printf("Sending response...\n");
const char *response =
"HTTP/1.1 200 OK\r\n"
"Connection: close\r\n"
"Content-Type: text/plain\r\n\r\n"
"Local time is: ";
int bytes_sent = SSL_write(ssl, response, strlen(response));
printf("Sent %d of %d bytes.\n", bytes_sent, (int)strlen(response));
The time() and ctime() functions are then used to format the current time. Once the time is formatted in time_msg, it is also sent to the client using SSL_write(). The following code shows this:
/*tls_time_server.c continued*/
time_t timer;
time(&timer);
char *time_msg = ctime(&timer);
bytes_sent = SSL_write(ssl, time_msg, strlen(time_msg));
printf("Sent %d of %d bytes.\n", bytes_sent, (int)strlen(time_msg));
Finally, after the data is transmitted to the client, the connection is closed, and the loop repeats. The following code shows this:
/*tls_time_server.c continued*/
printf("Closing connection...\n");
SSL_shutdown(ssl);
CLOSESOCKET(socket_client);
SSL_free(ssl);
}
If the loop terminates, it would be useful to close the listening socket and clean up the SSL context, as demonstrated by the following code:
/*tls_time_server.c continued*/
printf("Closing listening socket...\n");
CLOSESOCKET(socket_listen);
SSL_CTX_free(ctx);
Finally, Winsock should be cleaned up if necessary:
/*tls_time_server.c continued*/
#if defined(_WIN32)
WSACleanup();
#endif
printf("Finished.\n");
return 0;
}
That concludes tls_time_server.c.
You can compile and run the program using the following commands on macOS or Linux:
gcc tls_time_server.c -o tls_time_server -lssl -lcrypto
./tls_time_server
On Windows, compiling and running the program is done with the following commands:
gcc tls_time_server.c -o tls_time_server.exe -lssl -lcrypto -lws2_32
tls_time_server
If you have linker errors, please be sure that the OpenSSL library is installed correctly. You may find it helpful to attempt to compile openssl_version.c from Chapter 9, Loading Secure Web Pages with HTTPS and OpenSSL.
The following screenshot shows what running tls_time_server might look like:
You can connect to the time server by navigating your web browser to https://127.0.0.1:8080. Upon the first connection, your browser will reject the self-signed certificate. The following screenshot shows what this rejection looks like in Firefox:
To access the time server, you need to add an exception in the browser. The method for this is different in each browser, but generally, there is an Advanced button that leads to an option to either add a certificate exception or otherwise proceed with the insecure connection.
Once the browser connection is established, you will be able to see the current time as given by our tls_time_server program:
The tls_time_server program proved useful to show how a TLS/SSL server can be set up without getting bogged down in the details of actualizing a complete HTTPS server. However, this chapter's code repository also includes a more substantial HTTPS server.