/* * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy * in the file LICENSE in the source distribution or at * https://www.openssl.org/source/license.html */ /** * @file quic-hq-interop.c * @brief QUIC client interoperability demo using OpenSSL. * * This file contains the implementation of a QUIC client that demonstrates * interoperability with hq-interop servers. It handles connection setup, * session caching, key logging, and sending HTTP GET requests over QUIC. * * The file includes functions for setting up SSL/TLS contexts, managing session * caches, and handling I/O failures. It supports both IPv4 and IPv6 connections * and uses non-blocking mode for QUIC operations. * * The client sends HTTP/1.0 GET requests for specified files and saves the * responses to disk. It demonstrates OpenSSL's QUIC API, including ALPN protocol * settings, peer address management, and SSL handshake and shutdown processes. * * @note This client is intended for demonstration purposes and may require * additional error handling and robustness improvements for production use. * * USAGE * quic-hq-interop * host - The hostname of the server to contact * port - The port that the server is listening on * reqfile - a text file containing a space separated list of paths to fetch * via http 1.0 GET requests * * ENVIRONMENT VARIABLES * SSLKEYLOGFILE - set to a file path to record keylog exchange with server * SSL_SESSION_FILE - set to a file path to record ssl sessions and restore * said sessions on next invocation * SSL_CERT_FILE - The ca file to use when validating a server */ #include /* Include the appropriate header file for SOCK_DGRAM */ #ifdef _WIN32 /* Windows */ # include #else /* Linux/Unix */ # include # include #endif #include #include #include static int handle_io_failure(SSL *ssl, int res); static int set_keylog_file(SSL_CTX *ctx, const char *keylog_file); #define REQ_STRING_SZ 1024 /** * @brief A static pointer to a BIO object representing the session's BIO. * * This variable holds a reference to a BIO object used for network * communication during the session. It is initialized to NULL and should * be assigned a valid BIO object before use. The BIO object pointed to by * this variable may be used throughout the session for reading and writing * data. * * @note This variable is static, meaning it is only accessible within the * file in which it is declared. */ static BIO *session_bio = NULL; /** * @brief A static pointer to a BIO object used for logging key material. * * This variable holds a reference to a BIO object that is used to log * cryptographic key material for debugging purposes. It is initialized to * NULL and should be assigned a valid BIO object before use. * * @note This variable is static, meaning it is only accessible within the * file in which it is declared. */ static BIO *bio_keylog = NULL; /** * @brief Creates a BIO object for a UDP socket connection to a server. * * This function attempts to create a UDP socket and connect it to the server * specified by the given hostname and port. The socket is wrapped in a BIO * object, which allows for network communication via OpenSSL's BIO API. * The function also returns the address of the connected peer. * * @param hostname The hostname of the server to connect to. * @param port The port number of the server to connect to. * @param family The desired address family (e.g., AF_INET for IPv4, * AF_INET6 for IPv6). * @param peer_addr A pointer to a BIO_ADDR pointer that will hold the address * of the connected peer on success. The caller is responsible * for freeing this memory using BIO_ADDR_free(). * @return A pointer to a BIO object on success, or NULL on failure. * The returned BIO object will be associated with the connected socket. * If the BIO object is successfully created, it will take ownership of * the socket and automatically close it when the BIO is freed. * * @note The function uses OpenSSL's socket-related functions (e.g., BIO_socket, * BIO_connect) or portability and to integrate with OpenSSL's error * handling mechanisms. * @note If no valid connection is established, the function returns NULL and * ensures that any resources allocated during the process are properly * freed. */ static BIO *create_socket_bio(const char *hostname, const char *port, int family, BIO_ADDR **peer_addr) { int sock = -1; BIO_ADDRINFO *res; const BIO_ADDRINFO *ai = NULL; BIO *bio; /* * Lookup IP address info for the server. */ if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0, &res)) return NULL; /* * Loop through all the possible addresses for the server and find one * we can connect to. */ for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) { /* * Create a UDP socket. We could equally use non-OpenSSL calls such * as "socket" here for this and the subsequent connect and close * functions. But for portability reasons and also so that we get * errors on the OpenSSL stack in the event of a failure we use * OpenSSL's versions of these functions. */ sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0); if (sock == -1) continue; /* Connect the socket to the server's address */ if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) { BIO_closesocket(sock); sock = -1; continue; } /* * Set to nonblocking mode * Note: This function returns a range of errors * <= 0 if something goes wrong, so catch them all here */ if (BIO_socket_nbio(sock, 1) <= 0) { BIO_closesocket(sock); sock = -1; continue; } break; } if (sock != -1) { *peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai)); if (*peer_addr == NULL) { BIO_closesocket(sock); return NULL; } } /* Free the address information resources we allocated earlier */ BIO_ADDRINFO_free(res); /* If sock is -1 then we've been unable to connect to the server */ if (sock == -1) return NULL; /* Create a BIO to wrap the socket */ bio = BIO_new(BIO_s_datagram()); if (bio == NULL) { BIO_closesocket(sock); return NULL; } /* * Associate the newly created BIO with the underlying socket. By * passing BIO_CLOSE here the socket will be automatically closed when * the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which * case you must close the socket explicitly when it is no longer * needed. */ if (BIO_set_fd(bio, sock, BIO_CLOSE) <= 0) { BIO_closesocket(sock); BIO_free(bio); return NULL; } return bio; } /** * @brief Waits for activity on the SSL socket, either for reading or writing. * * This function monitors the underlying file descriptor of the given SSL * connection to determine when it is ready for reading or writing, or both. * It uses the select function to wait until the socket is either readable * or writable, depending on what the SSL connection requires. * * @param ssl A pointer to the SSL object representing the connection. * * @note This function blocks until there is activity on the socket or * until the timeout specified by OpenSSL is reached. In a real application, * you might want to perform other tasks while waiting, such as updating a * GUI or handling other connections. * * @note This function uses select for simplicity and portability. Depending * on your application's requirements, you might consider using other * mechanisms like poll or epoll for handling multiple file descriptors. */ static void wait_for_activity(SSL *ssl) { fd_set wfds, rfds; int width, sock, isinfinite; struct timeval tv; struct timeval *tvp = NULL; /* Get hold of the underlying file descriptor for the socket */ sock = SSL_get_fd(ssl); FD_ZERO(&wfds); FD_ZERO(&rfds); /* * Find out if we would like to write to the socket, or read from it (or * both) */ if (SSL_net_write_desired(ssl)) FD_SET(sock, &wfds); if (SSL_net_read_desired(ssl)) FD_SET(sock, &rfds); width = sock + 1; /* * Find out when OpenSSL would next like to be called, regardless of * whether the state of the underlying socket has changed or not. */ if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite) tvp = &tv; /* * Wait until the socket is writeable or readable. We use select here * for the sake of simplicity and portability, but you could equally use * poll/epoll or similar functions * * NOTE: For the purposes of this demonstration code this effectively * makes this demo block until it has something more useful to do. In a * real application you probably want to go and do other work here (e.g. * update a GUI, or service other connections). * * Let's say for example that you want to update the progress counter on * a GUI every 100ms. One way to do that would be to use the timeout in * the last parameter to "select" below. If the tvp value is greater * than 100ms then use 100ms instead. Then, when select returns, you * check if it did so because of activity on the file descriptors or * because of the timeout. If the 100ms GUI timeout has expired but the * tvp timeout has not then go and update the GUI and then restart the * "select" (with updated timeouts). */ select(width, &rfds, &wfds, NULL, tvp); } /** * @brief Handles I/O failures on an SSL connection based on the result code. * * This function processes the result of an SSL I/O operation and handles * different types of errors that may occur during the operation. It takes * appropriate actions such as retrying the operation, reporting errors, or * returning specific status codes based on the error type. * * @param ssl A pointer to the SSL object representing the connection. * @param res The result code from the SSL I/O operation. * @return An integer indicating the outcome: * - 1: Temporary failure, the operation should be retried. * - 0: EOF, indicating the connection has been closed. * - -1: A fatal error occurred or the connection has been reset. * * @note This function may block if a temporary failure occurs and * wait_for_activity() is called. * * @note If the failure is due to an SSL verification error, additional * information will be logged to stderr. */ static int handle_io_failure(SSL *ssl, int res) { switch (SSL_get_error(ssl, res)) { case SSL_ERROR_WANT_READ: case SSL_ERROR_WANT_WRITE: /* Temporary failure. Wait until we can read/write and try again */ wait_for_activity(ssl); return 1; case SSL_ERROR_ZERO_RETURN: /* EOF */ return 0; case SSL_ERROR_SYSCALL: return -1; case SSL_ERROR_SSL: /* * Some stream fatal error occurred. This could be because of a * stream reset - or some failure occurred on the underlying * connection. */ switch (SSL_get_stream_read_state(ssl)) { case SSL_STREAM_STATE_RESET_REMOTE: fprintf(stderr, "Stream reset occurred\n"); /* * The stream has been reset but the connection is still * healthy. */ break; case SSL_STREAM_STATE_CONN_CLOSED: fprintf(stderr, "Connection closed\n"); /* Connection is already closed. */ break; default: fprintf(stderr, "Unknown stream failure\n"); break; } /* * If the failure is due to a verification error we can get more * information about it from SSL_get_verify_result(). */ if (SSL_get_verify_result(ssl) != X509_V_OK) fprintf(stderr, "Verify error: %s\n", X509_verify_cert_error_string(SSL_get_verify_result(ssl))); return -1; default: return -1; } } /** * @brief Callback function to log key material during an SSL session. * * This function is invoked by OpenSSL when key material needs to be logged * for debugging purposes. It writes the provided key log line to the * `bio_keylog` BIO, ensuring thread-safe output by writing the entire line * at once. * * @param ssl A pointer to the SSL object associated with the session. * @param line The key log line to be written. * * @note If `bio_keylog` is NULL, an error message is printed to stderr, and * the function returns without logging the key material. */ static void keylog_callback(const SSL *ssl, const char *line) { if (bio_keylog == NULL) { fprintf(stderr, "Keylog callback is invoked without valid file!\n"); return; } /* * There might be concurrent writers to the keylog file, so we must ensure * that the given line is written at once. */ BIO_printf(bio_keylog, "%s\n", line); (void)BIO_flush(bio_keylog); } /** * @brief Sets up the key logging file for an SSL context. * * This function configures a file to log SSL/TLS key material for the * provided SSL context. If a keylog file is specified, it will be opened * in append mode, allowing for concurrent writes and preserving existing * logs. If no keylog file is provided, key logging is disabled. * * @param ctx A pointer to the SSL_CTX object where the keylog file is set. * @param keylog_file The path to the keylog file. If NULL, key logging is * disabled. * @return 0 on success, or 1 if there was an error opening the keylog file. * * @note The function writes a header to the keylog file if it is empty and * seekable. It also ensures that any previously opened keylog files are * closed before opening a new one. */ static int set_keylog_file(SSL_CTX *ctx, const char *keylog_file) { /* Close any open files */ BIO_free_all(bio_keylog); bio_keylog = NULL; if (ctx == NULL || keylog_file == NULL) { /* Keylogging is disabled, OK. */ return 0; } /* * Append rather than write in order to allow concurrent modification. * Furthermore, this preserves existing keylog files which is useful when * the tool is run multiple times. */ bio_keylog = BIO_new_file(keylog_file, "a"); if (bio_keylog == NULL) { printf("Error writing keylog file %s\n", keylog_file); return 1; } /* Write a header for seekable, empty files (this excludes pipes). */ if (BIO_tell(bio_keylog) == 0) { BIO_puts(bio_keylog, "# SSL/TLS secrets log file, generated by OpenSSL\n"); (void)BIO_flush(bio_keylog); } SSL_CTX_set_keylog_callback(ctx, keylog_callback); return 0; } /** * @brief A static integer indicating whether the session is cached. * * This variable is used to track the state of session caching. It is * initialized to 0, meaning no session is cached. The value may be updated * to indicate that a session has been successfully cached. * * @note This variable is static, meaning it is only accessible within the * file in which it is declared. */ static int session_cached = 0; /** * @brief Caches a new SSL session if one is not already cached. * * This function writes a new SSL session to the session BIO and caches it. * It ensures that only one session is cached at a time by checking the * `session_cached` flag. If a session is already cached, the function * returns without caching the new session. * * @param ssl A pointer to the SSL object associated with the session. * @param sess A pointer to the SSL_SESSION object to be cached. * @return 1 if the session is successfully cached, 0 otherwise. * * @note This function only allows one session to be cached. Subsequent * sessions will not be cached unless `session_cached` is reset. */ static int cache_new_session(struct ssl_st *ssl, SSL_SESSION *sess) { if (session_cached == 1) return 0; /* Just write the new session to our bio */ if (!PEM_write_bio_SSL_SESSION(session_bio, sess)) return 0; (void)BIO_flush(session_bio); /* only cache one session */ session_cached = 1; return 1; } /** * @brief Sets up the session cache for the SSL connection. * * This function configures session caching for the given SSL connection * and context. It attempts to load a session from the specified cache file * or creates a new one if the file does not exist. It also configures the * session cache mode and disables stateless session tickets. * * @param ssl A pointer to the SSL object for the connection. * @param ctx A pointer to the SSL_CTX object representing the context. * @param filename The name of the file used to store the session cache. * @return 1 on success, 0 on failure. * * @note If the cache file does not exist, a new file is created and the * session cache is initialized. If a session is successfully loaded from * the file, it is added to the context and set for the SSL connection. * If an error occurs during setup, the session BIO is freed. */ static int setup_session_cache(SSL *ssl, SSL_CTX *ctx, const char *filename) { SSL_SESSION *sess = NULL; int rc = 0; int new_cache = 0; /* * Because we cache sessions to a file in this client, we don't * actualy need to internally store sessions, because we restore them * from the file with SSL_set_session below, but we want to ensure * that caching is enabled so that the session cache callbacks get called * properly. The documentation is a bit unclear under what conditions * the callback is made, so play it safe here, by enforcing enablement */ if (!SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE | SSL_SESS_CACHE_NO_AUTO_CLEAR)) return rc; /* open our cache file */ session_bio = BIO_new_file(filename, "r+"); if (session_bio == NULL) { /* file might need to be created */ session_bio = BIO_new_file(filename, "w+"); if (session_bio == NULL) return rc; new_cache = 1; } if (new_cache == 0) { /* read in our cached session */ if (PEM_read_bio_SSL_SESSION(session_bio, &sess, NULL, NULL)) { /* set our session */ if (!SSL_set_session(ssl, sess)) goto err; } } else { /* Set the callback to store new sessions */ SSL_CTX_sess_set_new_cb(ctx, cache_new_session); } rc = 1; err: if (rc == 0) BIO_free(session_bio); return rc; } /** * @brief Pointer to a list of SSL polling items. * * This static variable holds the reference to a dynamically allocated list * of SSL_POLL_ITEM structures used for SSL polling operations. It is * initialized to NULL and will be populated as needed. */ static SSL_POLL_ITEM *poll_list = NULL; /** * @brief Pointer to an array of BIO objects for output. * * This static variable holds the reference to a dynamically allocated array * of BIO structures, which are used for handling output in SSL operations. * It is initialized to NULL and will be set when needed. This array holds * the out bio's for all received data from GET requests */ static BIO **outbiolist = NULL; /** * @brief Pointer to an array of output names. * * This static variable holds the reference to a dynamically allocated array * of strings, representing output names. It is initialized to NULL and * populated as required during operation. This array holds the names of the * output files from http GET requests. Indicies are correlated with the * corresponding outbiolist and poll_list arrays */ static char **outnames = NULL; /** * @brief Counter for the number of poll items. * * This static variable holds the count of items in the poll_list. It is * initialized to 0 and updated as items are added or removed from the list. */ static size_t poll_count = 0; /** * @brief Pointer to an array of request strings. * * This static variable holds the reference to a dynamically allocated array * of strings, representing requests. It is initialized to NULL and populated * as requests are added during execution. */ static char **req_array = NULL; /** * @brief Counter for the total number of requests. * * This static variable tracks the total number of parsed from reqfile. It is * initialized to 0 and incremented as new requests are processed. */ static size_t total_requests = 0; /** * @brief Index for the current request in the request array. * * This static variable keeps track of the index of the current request being * processed in the request array. It is initialized to 0 and updated as * requests are handled. */ static size_t req_idx = 0; /** * @brief Builds and processes a set of SSL poll requests. * * This function creates a new set of SSL poll requests based on the current * request array. It allocates and manages memory for poll lists, BIO output * files, and associated request names. Each request sends an HTTP GET to the * corresponding peer. The function processes the requests until a batch limit * or error is encountered. * * @param ssl A pointer to the SSL object to use for creating new streams. * * @return The number of poll requests successfully built, or 0 on error. */ static size_t build_request_set(SSL *ssl) { size_t poll_idx; char *req; char outfilename[REQ_STRING_SZ]; char req_string[REQ_STRING_SZ]; SSL *new_stream; size_t written; /* * Free any previous poll list */ for (poll_idx = 0; poll_idx < poll_count; poll_idx++) { (void)BIO_flush(outbiolist[poll_idx]); BIO_free(outbiolist[poll_idx]); SSL_free(poll_list[poll_idx].desc.value.ssl); } /* * Reset out lists and poll_count */ OPENSSL_free(outbiolist); OPENSSL_free(outnames); OPENSSL_free(poll_list); outnames = NULL; poll_list = NULL; outbiolist = NULL; poll_count = 0; /* * Iterate through our parsed lists of requests * note req_idx may start at a non-zero value if * multiple calls to build_request_list are made */ while (req_idx < total_requests) { req = req_array[req_idx]; /* Up our poll count and set our poll_list index */ poll_count++; poll_idx = poll_count - 1; /* * Expand our poll_list, outbiolist, and outnames arrays */ poll_list = OPENSSL_realloc(poll_list, sizeof(SSL_POLL_ITEM) * poll_count); if (poll_list == NULL) { fprintf(stderr, "Unable to realloc poll_list\n"); goto err; } outbiolist = OPENSSL_realloc(outbiolist, sizeof(BIO *) * poll_count); if (outbiolist == NULL) { fprintf(stderr, "Unable to realloc outbiolist\n"); goto err; } outnames = OPENSSL_realloc(outnames, sizeof(char *) * poll_count); if (outnames == NULL) { fprintf(stderr, "Unable to realloc outnames\n"); goto err; } /* set the output file name for this index */ outnames[poll_idx] = req; /* Format the http request */ BIO_snprintf(req_string, REQ_STRING_SZ, "GET /%s\r\n", req); /* build the outfile request path */ memset(outfilename, 0, REQ_STRING_SZ); BIO_snprintf(outfilename, REQ_STRING_SZ, "/downloads/%s", req); /* open a bio to write the file */ outbiolist[poll_idx] = BIO_new_file(outfilename, "w+"); if (outbiolist[poll_idx] == NULL) { fprintf(stderr, "Failed to open outfile %s\n", outfilename); goto err; } /* create a request stream */ new_stream = NULL; /* * We don't strictly have to do this check, but our quic client limits * our max data streams to 100, so we're just batching in groups of 100 * for now */ if (poll_count <= 99) new_stream = SSL_new_stream(ssl, 0); if (new_stream == NULL) { /* * We ran out of new streams to allocate * return and process this batch before getting more */ poll_count--; return poll_count; } /* * Create a poll descriptor for this stream */ poll_list[poll_idx].desc = SSL_as_poll_descriptor(new_stream); poll_list[poll_idx].revents = 0; poll_list[poll_idx].events = SSL_POLL_EVENT_R; /* Write an HTTP GET request to the peer */ while (!SSL_write_ex2(poll_list[poll_idx].desc.value.ssl, req_string, strlen(req_string), SSL_WRITE_FLAG_CONCLUDE, &written)) { if (handle_io_failure(poll_list[poll_idx].desc.value.ssl, 0) == 1) continue; /* Retry */ fprintf(stderr, "Failed to write start of HTTP request\n"); goto err; /* Cannot retry: error */ } req_idx++; } return poll_count; err: for (poll_idx = 0; poll_idx < poll_count; poll_idx++) { BIO_free(outbiolist[poll_idx]); SSL_free(poll_list[poll_idx].desc.value.ssl); } OPENSSL_free(poll_list); OPENSSL_free(outbiolist); poll_list = NULL; outbiolist = NULL; poll_count = 0; return poll_count; } /** * @brief Static pointer to a BIO_ADDR structure representing the peer's address. * * This variable is used to store the address of a peer for network communication. * It is statically allocated and should be initialized appropriately. */ static BIO_ADDR *peer_addr = NULL; /** * @brief Set up a TLS/QUIC connection to the specified hostname and port. * * This function creates and configures an SSL context for a client connection * using the QUIC client method. It sets up the necessary certificates, * performs host verification, configures ALPN, and establishes a non-blocking * connection. * * @param hostname Hostname to connect to. * @param port Port to connect to. * @param ipv6 Whether to use IPv6 (non-zero for IPv6, zero for IPv4). * @param ctx Pointer to an SSL_CTX object, which will be created. * @param ssl Pointer to an SSL object, which will be created. * * @return Returns 0 on success, 1 on error. */ static int setup_connection(char *hostname, char *port, int ipv6, SSL_CTX **ctx, SSL **ssl) { unsigned char alpn[] = {10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p'}; int ret = 0; char *sslkeylogfile = NULL; BIO *bio = NULL; /* * Create an SSL_CTX which we can use to create SSL objects from. We * want an SSL_CTX for creating clients so we use * OSSL_QUIC_client_method() here. */ *ctx = SSL_CTX_new(OSSL_QUIC_client_method()); if (*ctx == NULL) { fprintf(stderr, "Failed to create the SSL_CTX\n"); goto end; } /* * Configure the client to abort the handshake if certificate * verification fails. Virtually all clients should do this unless you * really know what you are doing. */ SSL_CTX_set_verify(*ctx, SSL_VERIFY_PEER, NULL); /* * Use the default trusted certificate store * Note: The store is read from SSL_CERT_DIR and SSL_CERT_FILE * environment variables in the default case, so users can set those * When running this application to direct where the store is loaded from */ if (!SSL_CTX_set_default_verify_paths(*ctx)) { fprintf(stderr, "Failed to set the default trusted certificate store\n"); goto end; } sslkeylogfile = getenv("SSLKEYLOGFILE"); if (sslkeylogfile != NULL) if (set_keylog_file(*ctx, sslkeylogfile)) goto end; /* Create an SSL object to represent the TLS connection */ *ssl = SSL_new(*ctx); if (*ssl == NULL) { fprintf(stderr, "Failed to create the SSL object\n"); goto end; } if (getenv("SSL_SESSION_FILE") != NULL) { if (!setup_session_cache(*ssl, *ctx, getenv("SSL_SESSION_FILE"))) { fprintf(stderr, "Unable to setup session cache\n"); goto end; } } /* * Create the underlying transport socket/BIO and associate it with the * connection. */ bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET, &peer_addr); if (bio == NULL) { fprintf(stderr, "Failed to crete the BIO\n"); goto end; } SSL_set_bio(*ssl, bio, bio); /* * Tell the server during the handshake which hostname we are attempting * to connect to in case the server supports multiple hosts. */ if (!SSL_set_tlsext_host_name(*ssl, hostname)) { fprintf(stderr, "Failed to set the SNI hostname\n"); goto end; } /* * Ensure we check during certificate verification that the server has * supplied a certificate for the hostname that we were expecting. * Virtually all clients should do this unless you really know what you * are doing. */ if (!SSL_set1_host(*ssl, hostname)) { fprintf(stderr, "Failed to set the certificate verification hostname"); goto end; } /* SSL_set_alpn_protos returns 0 for success! */ if (SSL_set_alpn_protos(*ssl, alpn, sizeof(alpn)) != 0) { fprintf(stderr, "Failed to set the ALPN for the connection\n"); goto end; } /* Set the IP address of the remote peer */ if (!SSL_set1_initial_peer_addr(*ssl, peer_addr)) { fprintf(stderr, "Failed to set the initial peer address\n"); goto end; } /* * The underlying socket is always nonblocking with QUIC, but the default * behaviour of the SSL object is still to block. We set it for nonblocking * mode in this demo. */ if (!SSL_set_blocking_mode(*ssl, 0)) { fprintf(stderr, "Failed to turn off blocking mode\n"); goto end; } /* Do the handshake with the server */ while ((ret = SSL_connect(*ssl)) != 1) { if (handle_io_failure(*ssl, ret) == 1) continue; /* Retry */ fprintf(stderr, "Failed to connect to server\n"); goto end; /* Cannot retry: error */ } return 1; end: SSL_CTX_free(*ctx); SSL_free(*ssl); BIO_ADDR_free(peer_addr); return 0; } /** * @brief Entry point for the QUIC hq-interop client demo application. * * This function sets up an SSL/TLS connection using QUIC, sends HTTP GET * requests for files specified in the command-line arguments, and saves * the responses to disk. It handles various configurations such as IPv6 * support, session caching, and key logging. * * @param argc The number of command-line arguments. * @param argv The array of command-line arguments. The expected format is * "[-6] hostname port file". * @return EXIT_SUCCESS on success, or EXIT_FAILURE on error. * * @note The function performs the following main tasks: * - Parses command-line arguments and configures IPv6 if specified. * - Reads the list of requests from the specified file. * - Sets up the SSL context and configures certificate verification. * - Optionally enables key logging and session caching. * - Establishes a non-blocking QUIC connection to the server. * - Sends an HTTP GET request for each file and writes the response * to the corresponding output file. * - Gracefully shuts down the SSL connection and frees resources. * - Prints any OpenSSL error stack information on failure. */ int main(int argc, char *argv[]) { SSL_CTX *ctx = NULL; SSL *ssl = NULL; BIO *req_bio = NULL; int res = EXIT_FAILURE; int ret; size_t readbytes = 0; char buf[160]; int eof = 0; int argnext = 1; char *reqfile = NULL; char *reqnames = OPENSSL_zalloc(1025); size_t read_offset = 0; size_t bytes_read = 0; size_t poll_idx = 0; size_t poll_done = 0; size_t result_count = 0; struct timeval poll_timeout; size_t this_poll_count = 0; char *req = NULL; char *hostname, *port; int ipv6 = 0; if (argc < 4) { fprintf(stderr, "Usage: quic-hq-interop [-6] hostname port reqfile\n"); goto end; } if (!strcmp(argv[argnext], "-6")) { if (argc < 5) { fprintf(stderr, "Usage: quic-hq-interop [-6] hostname port reqfile\n"); goto end; } ipv6 = 1; argnext++; } hostname = argv[argnext++]; port = argv[argnext++]; reqfile = argv[argnext]; req_bio = BIO_new_file(reqfile, "r"); if (req_bio == NULL) { fprintf(stderr, "Failed to open request file %s\n", reqfile); goto end; } /* Get the list of requests */ while (!BIO_eof(req_bio)) { if (!BIO_read_ex(req_bio, &reqnames[read_offset], REQ_STRING_SZ, &bytes_read)) { fprintf(stderr, "Failed to read some data from request file\n"); goto end; } read_offset += bytes_read; reqnames = OPENSSL_realloc(reqnames, read_offset + REQ_STRING_SZ); if (reqnames == NULL) { fprintf(stderr, "Realloc failure\n"); goto end; } } BIO_free(req_bio); req_bio = NULL; reqnames[read_offset + 1] = '\0'; if (!setup_connection(hostname, port, ipv6, &ctx, &ssl)) { fprintf(stderr, "Unable to establish connection\n"); goto end; } req = strtok(reqnames, " "); while (req != NULL) { total_requests++; req_array = OPENSSL_realloc(req_array, sizeof(char *) * total_requests); req_array[total_requests - 1] = req; req = strtok(NULL, " "); } /* get a list of requests to poll */ this_poll_count = build_request_set(ssl); /* * Now poll all our descriptors for events */ while (this_poll_count != 0 && poll_done < this_poll_count) { result_count = 0; poll_timeout.tv_sec = 0; poll_timeout.tv_usec = 0; if (!SSL_poll(poll_list, this_poll_count, sizeof(SSL_POLL_ITEM), &poll_timeout, 0, &result_count)) { fprintf(stderr, "Failed to poll\n"); goto end; } /* Iterate over our poll array looking for ready SSL's */ for (poll_idx = 0; poll_idx < this_poll_count; poll_idx++) { /* * If we have visited the number of SSL's that SSL_poll * indicated were ready, we can go poll again */ if (result_count == 0) break; if (poll_list[poll_idx].revents == SSL_POLL_EVENT_R) { /* * We found an SSL that we can read, drop our result count */ result_count--; /* And clear the revents for the next poll */ poll_list[poll_idx].revents = 0; /* * Get up to sizeof(buf) bytes of the response. We keep reading until * the server closes the connection. */ eof = 0; /* Read our data, and handle any errors/eof conditions */ if (!SSL_read_ex(poll_list[poll_idx].desc.value.ssl, buf, sizeof(buf), &readbytes)) { switch (handle_io_failure(poll_list[poll_idx].desc.value.ssl, 0)) { case 1: eof = 0; break; /* Retry on next poll */ case 0: eof = 1; break; case -1: default: fprintf(stderr, "Failed reading remaining data\n"); goto end; /* Cannot retry: error */ } } /* * If error handling indicated that this SSL is in an EOF state * we mark the SSL as not needing any more polling, and up our * poll_done count. Otherwise, just write to the outbio */ if (!eof) { BIO_write(outbiolist[poll_idx], buf, readbytes); } else { fprintf(stderr, "completed %s\n", outnames[poll_idx]); /* This file is done, take it out of polling contention */ poll_list[poll_idx].events = 0; poll_done++; } } } /* * If we've completed this poll set, try get another one */ if (poll_done == this_poll_count) { this_poll_count = build_request_set(ssl); poll_done = 0; } } /* * Repeatedly call SSL_shutdown() until the connection is fully * closed. */ fprintf(stderr, "Shutting down\n"); while ((ret = SSL_shutdown(ssl)) != 1) { if (ret < 0 && handle_io_failure(ssl, ret) == 1) continue; /* Retry */ } /* Success! */ res = EXIT_SUCCESS; end: /* * If something bad happened then we will dump the contents of the * OpenSSL error stack to stderr. There might be some useful diagnostic * information there. */ if (res == EXIT_FAILURE) ERR_print_errors_fp(stderr); /* * Free the resources we allocated. We do not free the BIO object here * because ownership of it was immediately transferred to the SSL object * via SSL_set_bio(). The BIO will be freed when we free the SSL object. */ BIO_ADDR_free(peer_addr); OPENSSL_free(reqnames); BIO_free(session_bio); for (poll_idx = 0; poll_idx < poll_count; poll_idx++) { BIO_free(outbiolist[poll_idx]); SSL_free(poll_list[poll_idx].desc.value.ssl); } SSL_free(ssl); SSL_CTX_free(ctx); OPENSSL_free(outbiolist); OPENSSL_free(poll_list); return res; }