diff --git a/Makefile b/Makefile index 87831554..6f75bf58 100644 --- a/Makefile +++ b/Makefile @@ -69,7 +69,7 @@ src/rustls.h: src/*.rs cbindgen.toml target/$(PROFILE)/librustls_ffi.a: src/*.rs Cargo.toml RUSTFLAGS="-C metadata=rustls-ffi" ${CARGO} build $(CARGOFLAGS) -target/%.o: tests/%.c tests/common.h | target +target/%.o: tests/%.c tests/%.h tests/common.h | target $(CC) -o $@ -c $< $(CFLAGS) target/client: target/client.o target/common.o target/$(PROFILE)/librustls_ffi.a diff --git a/tests/client.c b/tests/client.c index f6cd27cf..250fb75d 100644 --- a/tests/client.c +++ b/tests/client.c @@ -1,3 +1,12 @@ +/* + * A simple demonstration client for rustls-ffi. + * + * This client connects to an HTTPS server, sends an HTTP GET request, and + * prints the response to stdout. + * + * Notably it _does not_ attempt to implement the semantics of HTTP 1.1 by + * parsing the response and processing content-length or chunked encoding. + */ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include @@ -12,388 +21,203 @@ #endif #include +#include #include #include #include #include -/* rustls.h is autogenerated in the Makefile using cbindgen. */ #include "rustls.h" #include "common.h" +#include "client.h" -/* - * Connect to the given hostname on the given port and return the file - * descriptor of the socket. Tries to connect up to 10 times. On error, - * print the error and return 1. Caller is responsible for closing socket. - */ int -make_conn(const char *hostname, const char *port) +main(const int argc, const char **argv) { - int sockfd = 0; - struct addrinfo *getaddrinfo_output = NULL, hints = { 0 }; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; /* looking for TCP */ +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(1, 1), &wsa); + setmode(STDOUT_FILENO, O_BINARY); +#endif + const rustls_client_config *tls_config = NULL; - LOG("connecting to %s:%s", hostname, port); - const int getaddrinfo_result = - getaddrinfo(hostname, port, &hints, &getaddrinfo_output); - if(getaddrinfo_result != 0) { - LOG("getaddrinfo: %s", gai_strerror(getaddrinfo_result)); - goto cleanup; - } + // Set the program name global variable for logging purposes. + programname = "client"; - int connect_result = -1; - for(int attempts = 0; attempts < 10; attempts++) { - LOG("connect attempt %d", attempts); - sockfd = socket(getaddrinfo_output->ai_family, - getaddrinfo_output->ai_socktype, - getaddrinfo_output->ai_protocol); - if(sockfd < 0) { - perror("client: making socket"); - sleep(1); - continue; - } - connect_result = connect( - sockfd, getaddrinfo_output->ai_addr, getaddrinfo_output->ai_addrlen); - if(connect_result < 0) { - if(sockfd > 0) { - close(sockfd); - } - perror("client: connecting"); - sleep(1); - continue; - } - break; - } - if(connect_result < 0) { - perror("client: connecting"); + // Process command line arguments. + int ret = 1; + if(argc <= 3) { + fprintf( + stderr, + "usage: %s hostname port path [numreqs]\n\n" + "Connect to a host via HTTPS on the provided port, make [numreqs] \n" + "requests for the given path, and emit each response to stdout.\n", + argv[0]); goto cleanup; } - const demo_result dr = nonblock(sockfd); - if(dr != DEMO_OK) { - return 1; - } - - freeaddrinfo(getaddrinfo_output); - return sockfd; + const char *hostname = argv[1]; + const char *port = argv[2]; + const char *path = argv[3]; + const char *numreqs = argc > 4 ? argv[4] : "3"; -cleanup: - if(getaddrinfo_output != NULL) { - freeaddrinfo(getaddrinfo_output); + char *end; + const long int numreqs_int = strtol(numreqs, &end, 10); + if(end == numreqs || *end != '\0') { + fprintf(stderr, "numreqs must be a positive integer\n"); + goto cleanup; } - if(sockfd > 0) { - close(sockfd); + if(numreqs_int <= 0) { + fprintf(stderr, "numreqs must be a positive integer\n"); + goto cleanup; } - return -1; -} - -static const char *CONTENT_LENGTH = "Content-Length"; - -/* - * Given an established TCP connection, and a rustls_connection, send an - * HTTP request and read the response. On success, return 0. On error, print - * the message and return 1. - */ -int -send_request_and_read_response(conndata *conn, rustls_connection *rconn, - const char *hostname, const char *path) -{ - const int sockfd = conn->fd; - int ret = 1; - char buf[2048]; - fd_set read_fds; - fd_set write_fds; - size_t n = 0; - const char *content_length_end; - unsigned long content_length = 0; - size_t headers_len = 0; - const rustls_str version = rustls_version(); - memset(buf, '\0', sizeof(buf)); - snprintf(buf, - sizeof(buf), - "GET %s HTTP/1.1\r\n" - "Host: %s\r\n" - "User-Agent: %.*s\r\n" - "Accept: carcinization/inevitable, text/html\r\n" - "Connection: close\r\n" - "\r\n", - path, - hostname, - (int)version.len, - version.data); - /* First we write the plaintext - the data that we want rustls to encrypt for - * us- to the rustls connection. */ - const rustls_result rr = - rustls_connection_write(rconn, (uint8_t *)buf, strlen(buf), &n); - if(rr != RUSTLS_RESULT_OK) { - LOG_SIMPLE("error writing plaintext bytes to rustls_connection"); + // Build a demo client options struct based on the environment. + demo_client_options opts = { 0 }; + if(options_from_env(&opts)) { goto cleanup; } - if(n != strlen(buf)) { - LOG_SIMPLE("short write writing plaintext bytes to rustls_connection"); + + // Build a rustls TLS client config with our client options. + tls_config = new_tls_config(&opts); + if(tls_config == NULL) { goto cleanup; } - for(;;) { - FD_ZERO(&read_fds); - /* These two calls just inspect the state of the connection - if it's time - for us to write more, or to read more. */ - if(rustls_connection_wants_read(rconn)) { - FD_SET(sockfd, &read_fds); - } - FD_ZERO(&write_fds); - if(rustls_connection_wants_write(rconn)) { - FD_SET(sockfd, &write_fds); - } + // Describe the connection we're about to make. + const demo_client_request_options req_opts = { + .tls_config = tls_config, + .hostname = hostname, + .port = port, + .path = path, + .use_vectored_io = opts.use_vectored_io, + }; - if(!rustls_connection_wants_read(rconn) && - !rustls_connection_wants_write(rconn)) { - LOG_SIMPLE( - "rustls wants neither read nor write. drain plaintext and exit"); - goto drain_plaintext; - } + // Make GET requests with the rustls client config. + for(int i = 0; i < numreqs_int; i++) { + LOG("request %d of %ld", i + 1, numreqs_int); - const int select_result = - select(sockfd + 1, &read_fds, &write_fds, NULL, NULL); - if(select_result == -1) { - perror("client: select"); + if(do_get_request(&req_opts)) { + LOG("request %d of %ld FAILED", i + 1, numreqs_int); goto cleanup; } - demo_result dr = DEMO_ERROR; - if(FD_ISSET(sockfd, &read_fds)) { - /* Read all bytes until we get EAGAIN. Then loop again to wind up in - select awaiting the next bit of data. */ - for(;;) { - dr = do_read(conn, rconn); - if(dr == DEMO_AGAIN) { - break; - } - else if(dr == DEMO_EOF) { - goto drain_plaintext; - } - else if(dr != DEMO_OK) { - goto cleanup; - } - if(headers_len == 0) { - const char *body = body_beginning(&conn->data); - if(body != NULL) { - headers_len = body - conn->data.data; - LOG("body began at %zu", headers_len); - const char *content_length_str = - get_first_header_value(conn->data.data, - headers_len, - CONTENT_LENGTH, - strlen(CONTENT_LENGTH), - &n); - if(content_length_str == NULL) { - LOG_SIMPLE("content length header not found"); - goto cleanup; - } - content_length = - strtoul(content_length_str, (char **)&content_length_end, 10); - if(content_length_end == content_length_str) { - LOG("invalid Content-Length '%.*s'", (int)n, content_length_str); - goto cleanup; - } - LOG("content length %lu", content_length); - } - } - if(headers_len != 0 && - conn->data.len >= headers_len + content_length) { - goto drain_plaintext; - } - } - } - if(FD_ISSET(sockfd, &write_fds)) { - for(;;) { - /* This invokes rustls_connection_write_tls. We pass a callback to - * that function. Rustls will pass a buffer to that callback with - * encrypted bytes, that we will write to `conn`. */ - const rustls_io_result err = write_tls(rconn, conn, &n); - if(err != 0) { - LOG("error in rustls_connection_write_tls: errno %d", err); - goto cleanup; - } - if(dr == DEMO_AGAIN) { - break; - } - if(n == 0) { - // Writing was successful, but we wrote 0 bytes. - break; - } - LOG("wrote %zu bytes of data to socket", n); - } - } + LOG("request %d of %ld successful", i + 1, numreqs_int); } + ret = 0; // Success. - LOG_SIMPLE("send_request_and_read_response: loop fell through"); - -drain_plaintext:; // NOTE: empty statement after label to allow var decls. - log_connection_info(rconn); +cleanup: + // Free the rustls TLS config. + rustls_client_config_free(tls_config); - const demo_result dr = copy_plaintext_to_buffer(conn); - if(dr != DEMO_OK && dr != DEMO_EOF) { - goto cleanup; - } - LOG("writing %zu bytes to stdout", conn->data.len); - if(write(STDOUT_FILENO, conn->data.data, conn->data.len) < 0) { - LOG_SIMPLE("error writing to stderr"); - goto cleanup; - } - ret = 0; +#ifdef _WIN32 + WSACleanup(); +#endif -cleanup: - if(sockfd > 0) { - close(sockfd); - } return ret; } -demo_result -do_request(const rustls_client_config *client_config, const char *hostname, - const char *port, - const char *path) // NOLINT(bugprone-easily-swappable-parameters) +int +options_from_env(demo_client_options *opts) { - rustls_connection *rconn = NULL; - conndata *conn = NULL; - demo_result dr = DEMO_ERROR; - const int sockfd = make_conn(hostname, port); - if(sockfd < 0) { - // No perror because make_conn printed error already. - goto cleanup; - } + // Consider verifier options. + const char *use_platform_verifier = getenv("RUSTLS_PLATFORM_VERIFIER"); + const char *use_ca_certificate_verifier = getenv("CA_FILE"); + const char *use_no_verifier = getenv("NO_CHECK_CERTIFICATE"); - const rustls_result rr = - rustls_client_connection_new(client_config, hostname, &rconn); - if(rr != RUSTLS_RESULT_OK) { - print_error("client_connection_new", rr); - goto cleanup; + if(use_platform_verifier) { + LOG_SIMPLE("using the platform verifier for certificate verification."); + opts->use_platform_verifier = true; } - - conn = calloc(1, sizeof(conndata)); - if(conn == NULL) { - goto cleanup; + else if(use_ca_certificate_verifier) { + LOG("using the CA file '%s' for certificate verification.", + use_ca_certificate_verifier); + opts->use_ca_certificate_verifier = use_ca_certificate_verifier; } - conn->rconn = rconn; - conn->fd = sockfd; - conn->verify_arg = "verify_arg"; - - rustls_connection_set_userdata(rconn, conn); - rustls_connection_set_log_callback(rconn, log_cb); - - dr = send_request_and_read_response(conn, rconn, hostname, path); - if(dr != DEMO_OK) { - goto cleanup; + else if(use_no_verifier) { + LOG_SIMPLE("skipping certificate verification (DANGER!)."); + opts->use_no_verifier = true; } - - dr = DEMO_OK; - -cleanup: - rustls_connection_free(rconn); - if(sockfd > 0) { - close(sockfd); - } - if(conn != NULL) { - if(conn->data.data != NULL) { - free(conn->data.data); - } - free(conn); + else { + LOG_SIMPLE("must set RUSTLS_PLATFORM_VERIFIER, CA_FILE or " + "NO_CHECK_CERTIFICATE env var"); + return 1; } - return dr; -} - -uint32_t -verify(void *userdata, const rustls_verify_server_cert_params *params) -{ - size_t i = 0; - const rustls_slice_slice_bytes *intermediates = - params->intermediate_certs_der; - const size_t intermediates_len = rustls_slice_slice_bytes_len(intermediates); - const conndata *conn = (struct conndata *)userdata; - LOG("custom certificate verifier called for %.*s", - (int)params->server_name.len, - params->server_name.data); - LOG("end entity len: %zu", params->end_entity_cert_der.len); - LOG_SIMPLE("intermediates:"); - for(i = 0; i < intermediates_len; i++) { - const rustls_slice_bytes bytes = - rustls_slice_slice_bytes_get(intermediates, i); - if(bytes.data != NULL) { - LOG(" intermediate, len = %zu", bytes.len); - } + // Consider client auth options. + const char *auth_cert = getenv("AUTH_CERT"); + const char *auth_key = getenv("AUTH_KEY"); + if(auth_cert && auth_key) { + LOG("using client auth with cert '%s' and key '%s'", auth_cert, auth_key); + opts->use_auth_cert_file = auth_cert; + opts->use_auth_cert_key_file = auth_key; } - LOG("ocsp response len: %zu", params->ocsp_response.len); - if(0 != strcmp(conn->verify_arg, "verify_arg")) { - LOG("invalid argument to verify: %p", userdata); - return RUSTLS_RESULT_GENERAL; + else if(auth_cert || auth_key) { + LOG_SIMPLE("must set both or neither of AUTH_CERT and AUTH_KEY env vars"); + return 1; } - return RUSTLS_RESULT_OK; -} -int -main(const int argc, const char **argv) -{ - int ret = 1; - - if(argc <= 3) { - fprintf( - stderr, - "usage: %s hostname port path [numreqs]\n\n" - "Connect to a host via HTTPS on the provided port, make [numreqs] \n" - "requests for the given path, and emit each response to stdout.\n", - argv[0]); + // Consider SSLKEYLOGFILE options. + const char *sslkeylogfile = getenv("SSLKEYLOGFILE"); + const char *stderrkeylog = getenv("STDERRKEYLOG"); + if(sslkeylogfile && stderrkeylog) { + LOG_SIMPLE( + "must set at most one of SSLKEYLOGFILE or STDERRKEYLOG env vars"); return 1; } - const char *hostname = argv[1]; - const char *port = argv[2]; - const char *path = argv[3]; - const char *numreqs = argc > 4 ? argv[4] : "3"; + if(sslkeylogfile) { + opts->use_ssl_keylog_file = sslkeylogfile; + LOG("using SSLKEYLOGFILE '%s'", opts->use_ssl_keylog_file); + } + else if(stderrkeylog) { + opts->use_stderr_keylog = true; + LOG_SIMPLE("using stderr for keylog output"); + } - char *end; - const long int numreqs_int = strtol(numreqs, &end, 10); - if(end == numreqs || *end != '\0') { - fprintf(stderr, "numreqs must be a positive integer\n"); - return 1; + // Consider custom ciphersuite name option. + const char *custom_ciphersuite_name = getenv("RUSTLS_CIPHERSUITE"); + if(custom_ciphersuite_name) { + opts->custom_ciphersuite_name = custom_ciphersuite_name; + LOG("using custom ciphersuite '%s'", opts->custom_ciphersuite_name); } - if(numreqs_int <= 0) { - fprintf(stderr, "numreqs must be a positive integer\n"); - return 1; + + // Consider vectored I/O (if supported) +#if !defined(_WIN32) + if(getenv("USE_VECTORED_IO")) { + LOG_SIMPLE("using vectored I/O"); + opts->use_vectored_io = true; } +#endif - /* Set this global variable for logging purposes. */ - programname = "client"; + return 0; +} +const rustls_client_config * +new_tls_config(const demo_client_options *opts) +{ + const rustls_client_config *result = NULL; + if(opts == NULL) { + return result; + } + + // Initialize things we may need to clean up. const rustls_crypto_provider *custom_provider = NULL; rustls_client_config_builder *config_builder = NULL; - rustls_root_cert_store_builder *server_cert_root_store_builder = NULL; - const rustls_root_cert_store *server_cert_root_store = NULL; - const rustls_client_config *client_config = NULL; rustls_web_pki_server_cert_verifier_builder *server_cert_verifier_builder = NULL; rustls_server_cert_verifier *server_cert_verifier = NULL; - rustls_slice_bytes alpn_http11; - const rustls_certified_key *certified_key = NULL; - - alpn_http11.data = (unsigned char *)"http/1.1"; - alpn_http11.len = 8; - -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(1, 1), &wsa); - setmode(STDOUT_FILENO, O_BINARY); -#endif + rustls_root_cert_store_builder *server_cert_root_store_builder = NULL; + const rustls_root_cert_store *server_cert_root_store = NULL; - const char *custom_ciphersuite_name = getenv("RUSTLS_CIPHERSUITE"); - if(custom_ciphersuite_name != NULL) { + // First, construct the client config builder. If the user has requested + // a custom ciphersuite, we first build a custom crypto provider that + // has only that suite, and then build the config builder with that. + if(opts->custom_ciphersuite_name != NULL) { custom_provider = - default_provider_with_custom_ciphersuite(custom_ciphersuite_name); + default_provider_with_custom_ciphersuite(opts->custom_ciphersuite_name); if(custom_provider == NULL) { goto cleanup; } - printf("customized to use ciphersuite: %s\n", custom_ciphersuite_name); + LOG("customized to use ciphersuite: %s", opts->custom_ciphersuite_name); const rustls_result rr = rustls_client_config_builder_new_custom(custom_provider, @@ -401,7 +225,7 @@ main(const int argc, const char **argv) default_tls_versions_len, &config_builder); if(rr != RUSTLS_RESULT_OK) { - print_error("creating client config builder", rr); + print_error("creating custom client config builder", rr); goto cleanup; } } @@ -409,20 +233,21 @@ main(const int argc, const char **argv) config_builder = rustls_client_config_builder_new(); } - if(getenv("RUSTLS_PLATFORM_VERIFIER")) { + // Then configure a verifier for the client config builder. + if(opts->use_platform_verifier) { const rustls_result rr = rustls_platform_server_cert_verifier(&server_cert_verifier); if(rr != RUSTLS_RESULT_OK) { - print_error("client: failed to construct platform verifier", rr); + print_error("failed to construct platform verifier", rr); goto cleanup; } rustls_client_config_builder_set_server_verifier(config_builder, server_cert_verifier); } - else if(getenv("CA_FILE")) { + else if(opts->use_ca_certificate_verifier != NULL) { server_cert_root_store_builder = rustls_root_cert_store_builder_new(); rustls_result rr = rustls_root_cert_store_builder_load_roots_from_file( - server_cert_root_store_builder, getenv("CA_FILE"), true); + server_cert_root_store_builder, opts->use_ca_certificate_verifier, true); if(rr != RUSTLS_RESULT_OK) { print_error("loading trusted certificates", rr); goto cleanup; @@ -443,17 +268,28 @@ main(const int argc, const char **argv) rustls_client_config_builder_set_server_verifier(config_builder, server_cert_verifier); } - else if(getenv("NO_CHECK_CERTIFICATE")) { + else if(opts->use_no_verifier) { rustls_client_config_builder_dangerous_set_certificate_verifier( - config_builder, verify); + config_builder, unsafe_skip_verify); } - else { - LOG_SIMPLE("must set either RUSTLS_PLATFORM_VERIFIER or CA_FILE or " - "NO_CHECK_CERTIFICATE env var"); - goto cleanup; + + // Then configure client authentication if required. + if(opts->use_auth_cert_file != NULL && + opts->use_auth_cert_key_file != NULL) { + const rustls_certified_key *certified_key = load_cert_and_key( + opts->use_auth_cert_file, opts->use_auth_cert_key_file); + if(certified_key == NULL) { + goto cleanup; + } + rustls_client_config_builder_set_certified_key( + config_builder, &certified_key, 1); + // Per docs we are allowed to free the certified key after giving it to the + // builder. + rustls_certified_key_free(certified_key); } - if(getenv("SSLKEYLOGFILE")) { + // Then configure SSLKEYLOG as required + if(opts->use_ssl_keylog_file != NULL) { const rustls_result rr = rustls_client_config_builder_set_key_log_file(config_builder); if(rr != RUSTLS_RESULT_OK) { @@ -461,7 +297,7 @@ main(const int argc, const char **argv) goto cleanup; } } - else if(getenv("STDERRKEYLOG")) { + else if(opts->use_stderr_keylog) { const rustls_result rr = rustls_client_config_builder_set_key_log( config_builder, stderr_key_log_cb, NULL); if(rr != RUSTLS_RESULT_OK) { @@ -470,54 +306,481 @@ main(const int argc, const char **argv) } } - char *auth_cert = getenv("AUTH_CERT"); - char *auth_key = getenv("AUTH_KEY"); - if((auth_cert && !auth_key) || (!auth_cert && auth_key)) { - LOG_SIMPLE("must set both AUTH_CERT and AUTH_KEY env vars, or neither"); + // Then configure ALPN. + rustls_slice_bytes alpn_http11 = { .data = (unsigned char *)"http/1.1", + .len = 8 }; + rustls_result rr = rustls_client_config_builder_set_alpn_protocols( + config_builder, &alpn_http11, 1); + if(rr != RUSTLS_RESULT_OK) { + print_error("setting ALPN", rr); goto cleanup; } - else if(auth_cert && auth_key) { - certified_key = load_cert_and_key(auth_cert, auth_key); - if(certified_key == NULL) { + + // Finally consume the config_builder by trying to build it into a client + // config. We can't use the config_builder (even to free it!) after this + // point. + rr = rustls_client_config_builder_build(config_builder, &result); + config_builder = NULL; + if(rr != RUSTLS_RESULT_OK) { + print_error("building client config builder", rr); + goto cleanup; + } + +cleanup: + rustls_root_cert_store_builder_free(server_cert_root_store_builder); + rustls_root_cert_store_free(server_cert_root_store); + rustls_web_pki_server_cert_verifier_builder_free( + server_cert_verifier_builder); + rustls_server_cert_verifier_free(server_cert_verifier); + rustls_crypto_provider_free(custom_provider); + rustls_client_config_builder_free(config_builder); + return result; +} + +int +do_get_request(const demo_client_request_options *options) +{ + if(options == NULL || options->tls_config == NULL || + options->hostname == NULL || options->port == NULL || + options->path == NULL) { + return 1; + } + + int ret = 1; + LOG("making GET request to https://%s:%s%s", + options->hostname, + options->port, + options->path); + + // Construct a new connection to the server. + demo_client_connection *demo_conn = demo_client_connect(options); + + // Write a plaintext HTTP GET request. + if(demo_client_connection_write_get(demo_conn)) { + goto cleanup; + } + + // Process I/O with select(). + struct timeval timeout; + timeout.tv_sec = 4; // Picked arbitrarily. + timeout.tv_usec = 0; + fd_set read_fds; + fd_set write_fds; + for(;;) { + FD_ZERO(&read_fds); + FD_ZERO(&write_fds); + + if(rustls_connection_wants_read(demo_conn->rconn)) { + FD_SET(demo_conn->sockfd, &read_fds); + } + if(rustls_connection_wants_write(demo_conn->rconn)) { + FD_SET(demo_conn->sockfd, &write_fds); + } + + if(!rustls_connection_wants_read(demo_conn->rconn) && + !rustls_connection_wants_write(demo_conn->rconn)) { + LOG_SIMPLE("rustls wants neither read nor write. Breaking i/o loop."); + break; + } + + const int select_result = + select(demo_conn->sockfd + 1, &read_fds, &write_fds, NULL, &timeout); + if(select_result == -1) { + perror("client: select"); + goto cleanup; + } + else if(select_result == 0) { + LOG_SIMPLE("select timed out"); + break; + } + + // If we can read data from the socket, read it and pass it to rustls. + if(FD_ISSET(demo_conn->sockfd, &read_fds)) { + LOG_SIMPLE("doing TLS reads"); + const demo_result dr = demo_client_connection_read_tls(demo_conn); + if(dr == DEMO_ERROR || dr == DEMO_EOF) { + demo_conn->closing = true; + } + if(dr == DEMO_AGAIN) { + LOG_SIMPLE("reading from socket: EAGAIN or EWOULDBLOCK"); + continue; + } + } + + // If we can write data to the socket, write whatever rustls has queued. + if(FD_ISSET(demo_conn->sockfd, &write_fds)) { + LOG_SIMPLE("doing TLS writes"); + const demo_result dr = demo_client_connection_write_tls(demo_conn); + if(dr == DEMO_ERROR) { + demo_conn->closing = true; + } + if(dr == DEMO_AGAIN) { + LOG_SIMPLE("writing to socket: EAGAIN or EWOULDBLOCK"); + continue; + } + } + + // Handle closure. + if(demo_conn->closing) { + LOG("Connection closed. Clean? %s", + demo_conn->clean_closure ? "yes" : "no"); + // fail result if it wasn't a clean closure. + ret = !demo_conn->clean_closure; + break; + } + } + LOG_SIMPLE("I/O loop fell through"); + log_connection_info(demo_conn->rconn); + + // Print whatever is in the user data buffer. + // TODO(@cpu): refactor conndata struct to avoid "data data data" naming + // when digging in to the conndata's bytevec's data. + const char *data = demo_conn->data->data.data; + const size_t data_len = demo_conn->data->data.len; + if(data_len > 0) { + LOG("writing %zu plaintext response bytes to stdout", data_len); + if(write(STDOUT_FILENO, data, data_len) < 0) { + LOG_SIMPLE("error writing to stderr"); goto cleanup; } - rustls_client_config_builder_set_certified_key( - config_builder, &certified_key, 1); + } + else if(ret == 0) { + LOG_SIMPLE("no plaintext response data was read"); + ret = 1; } - rustls_client_config_builder_set_alpn_protocols( - config_builder, &alpn_http11, 1); +cleanup: + // Free connection resources and return. + demo_client_connection_free(demo_conn); + return ret; +} + +demo_client_connection * +demo_client_connect(const demo_client_request_options *options) +{ + if(options == NULL) { + return NULL; + } + + conndata *data = NULL; + demo_client_connection *demo_conn = NULL; + + demo_conn = calloc(1, sizeof(demo_client_connection)); + if(demo_conn == NULL) { + perror("demo_client_connection calloc"); + goto cleanup; + } + demo_conn->options = options; + + // Connect the TCP socket. + const int sockfd = connect_socket(demo_conn->options); + if(sockfd <= 0) { + perror("client: connect_socket"); + goto cleanup; + } + LOG_SIMPLE("socket connected"); + demo_conn->sockfd = sockfd; - const rustls_result rr = - rustls_client_config_builder_build(config_builder, &client_config); + // Construct the rustls request with the client config. + const rustls_result rr = rustls_client_connection_new( + options->tls_config, options->hostname, &demo_conn->rconn); if(rr != RUSTLS_RESULT_OK) { - print_error("building client config", rr); + print_error("client_connection_new", rr); goto cleanup; } - for(int i = 0; i < numreqs_int; i++) { - const demo_result dr = do_request(client_config, hostname, port, path); - if(dr != DEMO_OK) { - goto cleanup; + data = calloc(1, sizeof(conndata)); + if(data == NULL) { + perror("client: conndata calloc"); + goto cleanup; + } + data->rconn = demo_conn->rconn; + data->fd = demo_conn->sockfd; + data->verify_arg = "verify_arg"; + demo_conn->data = data; + + rustls_connection_set_userdata(demo_conn->rconn, data); + rustls_connection_set_log_callback(demo_conn->rconn, log_cb); + + return demo_conn; + +cleanup: + if(demo_conn != NULL) { + demo_client_connection_free(demo_conn); + } + + return NULL; +} + +int +connect_socket(const demo_client_request_options *options) +{ + if(options == NULL) { + return -1; + } + + int sockfd = 0; + struct addrinfo *getaddrinfo_output = NULL, hints = { 0 }; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; /* looking for TCP */ + + const int getaddrinfo_result = + getaddrinfo(options->hostname, options->port, &hints, &getaddrinfo_output); + if(getaddrinfo_result != 0) { + LOG("getaddrinfo: %s", gai_strerror(getaddrinfo_result)); + goto cleanup; + } + + int connect_result = -1; + for(int attempts = 0; attempts < MAX_CONNECT_ATTEMPTS; attempts++) { + LOG("connect attempt %d of %d", attempts + 1, MAX_CONNECT_ATTEMPTS); + sockfd = socket(getaddrinfo_output->ai_family, + getaddrinfo_output->ai_socktype, + getaddrinfo_output->ai_protocol); + if(sockfd < 0) { + perror("client: making socket"); + sleep(1); + continue; } + connect_result = connect( + sockfd, getaddrinfo_output->ai_addr, getaddrinfo_output->ai_addrlen); + if(connect_result < 0) { + if(sockfd > 0) { + close(sockfd); + } + perror("client: connecting"); + sleep(1); + continue; + } + break; + } + if(connect_result < 0) { + perror("client: connecting"); + goto cleanup; + } + const demo_result dr = nonblock(sockfd); + if(dr != DEMO_OK) { + // no need to perror() - nonblock() already did. + return -1; } - // Success! - ret = 0; + freeaddrinfo(getaddrinfo_output); + return sockfd; // Success cleanup: - rustls_root_cert_store_builder_free(server_cert_root_store_builder); - rustls_root_cert_store_free(server_cert_root_store); - rustls_web_pki_server_cert_verifier_builder_free( - server_cert_verifier_builder); - rustls_server_cert_verifier_free(server_cert_verifier); - rustls_certified_key_free(certified_key); - rustls_client_config_free(client_config); - rustls_crypto_provider_free(custom_provider); + if(getaddrinfo_output != NULL) { + freeaddrinfo(getaddrinfo_output); + } + if(sockfd > 0) { + close(sockfd); + } + return -1; +} -#ifdef _WIN32 - WSACleanup(); +int +demo_client_connection_write_get(const demo_client_connection *demo_conn) +{ + if(demo_conn == NULL || demo_conn->options == NULL) { + return 1; + } + + // Construct a plaintext HTTP request buffer. + const rustls_str version = rustls_version(); + char get_request_buf[2048]; + int get_request_size = + snprintf(get_request_buf, + sizeof(get_request_buf), + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: %.*s\r\n" + "Accept: carcinization/inevitable, text/html\r\n" + "Connection: close\r\n" + "\r\n", + demo_conn->options->path, + demo_conn->options->hostname, + (int)version.len, + version.data); + + // Write the plaintext to the rustls connection. + size_t n = 0; + const rustls_result rr = rustls_connection_write( + demo_conn->rconn, (uint8_t *)get_request_buf, get_request_size, &n); + if(rr != RUSTLS_RESULT_OK) { + LOG_SIMPLE( + "error writing plaintext GET request bytes to rustls_connection"); + return 1; + } + if(n != (size_t)get_request_size) { + LOG_SIMPLE( + "short write writing plaintext GET request bytes to rustls_connection"); + return 1; + } + + return 0; // Success. +} + +demo_result +demo_client_connection_read_tls(demo_client_connection *demo_conn) +{ + size_t n = 0; + + const rustls_io_result io_res = + rustls_connection_read_tls(demo_conn->rconn, read_cb, demo_conn->data, &n); + + if(io_res == EAGAIN) { + return DEMO_AGAIN; + } + else if(io_res != 0) { + LOG("reading from socket failed: errno %d", io_res); + return DEMO_ERROR; + } + + if(n == 0) { + LOG_SIMPLE("read 0 bytes from socket, connection closed"); + demo_conn->closing = true; + demo_conn->clean_closure = true; + return DEMO_EOF; + } + + rustls_result rr = rustls_connection_process_new_packets(demo_conn->rconn); + if(rr != RUSTLS_RESULT_OK) { + print_error("processing new TLS packets", rr); + return DEMO_ERROR; + } + LOG("read %zu TLS bytes from socket", n); + + bytevec *bv = &demo_conn->data->data; + if(bytevec_ensure_available(bv, 1024) != DEMO_OK) { + return DEMO_ERROR; + } + + for(;;) { + char *buf = bytevec_writeable(bv); + const size_t avail = bytevec_available(bv); + + rr = rustls_connection_read(demo_conn->rconn, (uint8_t *)buf, avail, &n); + if(rr == RUSTLS_RESULT_PLAINTEXT_EMPTY) { + /* This is expected. It just means "no more bytes for now." */ + return DEMO_OK; + } + if(rr != RUSTLS_RESULT_OK) { + print_error("error in rustls_connection_read", rr); + return DEMO_ERROR; + } + if(n == 0) { + break; + } + bytevec_consume(bv, n); + if(bytevec_ensure_available(bv, 1024) != DEMO_OK) { + return DEMO_ERROR; + } + } + + /* If we got an EOF on the plaintext stream (peer closed connection cleanly), + * verify that the sender then closed the TCP connection. */ + char buf[1]; + const ssize_t signed_n = read(demo_conn->sockfd, buf, sizeof(buf)); + if(signed_n > 0) { + LOG("error: read returned %zu bytes after receiving close_notify", n); + return DEMO_ERROR; + } + else if(signed_n < 0 && errno != EWOULDBLOCK) { + LOG("wrong error after receiving close_notify: %s", strerror(errno)); + return DEMO_ERROR; + } + demo_conn->closing = true; + demo_conn->clean_closure = true; + return DEMO_EOF; +} + +demo_result +demo_client_connection_write_tls(const demo_client_connection *demo_conn) +{ + if(demo_conn == NULL || demo_conn->options == NULL) { + return DEMO_ERROR; + } + + size_t n; + rustls_io_result io_res; + +#if !defined(_WIN32) + if(demo_conn->options->use_vectored_io) { + io_res = rustls_connection_write_tls_vectored( + demo_conn->rconn, write_vectored_cb, demo_conn->data, &n); + } + else { + io_res = rustls_connection_write_tls( + demo_conn->rconn, write_cb, demo_conn->data, &n); + } +#else + io_res = rustls_connection_write_tls( + demo_conn->rconn, write_cb, demo_conn->data, &n); #endif - return ret; + if(io_res == EAGAIN) { + return DEMO_AGAIN; + } + else if(io_res != 0) { + LOG("writing to socket failed: errno %d", io_res); + return DEMO_ERROR; + } + + LOG("wrote %zu bytes of data to socket", n); + return DEMO_OK; +} + +void +demo_client_connection_free(demo_client_connection *conn) +{ + if(conn == NULL) { + return; + } + + if(conn->rconn != NULL) { + rustls_connection_free(conn->rconn); + } + + if(conn->data != NULL) { + conndata *data = conn->data; + if(data->data.data != NULL) { + free(data->data.data); + } + free(data); + } + + if(conn->sockfd != 0) { + close(conn->sockfd); + } + + free(conn); +} + +uint32_t +unsafe_skip_verify(void *userdata, + const rustls_verify_server_cert_params *params) +{ + size_t i = 0; + const rustls_slice_slice_bytes *intermediates = + params->intermediate_certs_der; + const size_t intermediates_len = rustls_slice_slice_bytes_len(intermediates); + const conndata *conn = (struct conndata *)userdata; + + LOG("custom certificate verifier called for %.*s", + (int)params->server_name.len, + params->server_name.data); + LOG("end entity len: %zu", params->end_entity_cert_der.len); + LOG_SIMPLE("intermediates:"); + for(i = 0; i < intermediates_len; i++) { + const rustls_slice_bytes bytes = + rustls_slice_slice_bytes_get(intermediates, i); + if(bytes.data != NULL) { + LOG(" intermediate, len = %zu", bytes.len); + } + } + LOG("ocsp response len: %zu", params->ocsp_response.len); + if(0 != strcmp(conn->verify_arg, "verify_arg")) { + LOG("invalid argument to verify: %p", userdata); + return RUSTLS_RESULT_GENERAL; + } + return RUSTLS_RESULT_OK; } diff --git a/tests/client.h b/tests/client.h new file mode 100644 index 00000000..e429cabe --- /dev/null +++ b/tests/client.h @@ -0,0 +1,128 @@ +#ifndef CLIENT_H +#define CLIENT_H + +#include + +#include "rustls.h" +#include "common.h" + +// A structure to hold client demo option state. +typedef struct demo_client_options +{ + // Options for certificate verification. Only one should be set. + bool use_no_verifier; + bool use_platform_verifier; + const char *use_ca_certificate_verifier; + + // Optional client authentication using a certificate/key. None, or both + // must be set. + const char *use_auth_cert_file; + const char *use_auth_cert_key_file; + + // Optional SSL keylog support. At most one should be set. + const char *use_ssl_keylog_file; + bool use_stderr_keylog; + + // Optional custom ciphersuite name. If set _only_ this ciphersuite + // will be used. + const char *custom_ciphersuite_name; + + // Optional vectored IO support. + bool use_vectored_io; +} demo_client_options; + +// Populate the provided options with values from the environment. +// Returns 0 on success, or non-zero on failure after printing error +// messages to stderr. +int options_from_env(demo_client_options *opts); + +// Construct a rustls_client_config based on the provided demo_client_options. +// +// Caller owns the returned rustls_client_config and must free it with +// rustls_client_config_free. The rustls_client_config must out-live any +// demo_client_request_options made referencing it. +// +// Returns NULL on failure after printing error messages to stderr. +const rustls_client_config *new_tls_config(const demo_client_options *opts); + +// Options for an HTTPS GET request. +typedef struct demo_client_request_options +{ + const rustls_client_config *tls_config; + const char *hostname; + const char *port; + const char *path; + bool use_vectored_io; +} demo_client_request_options; + +// Make an HTTP request based on the provided options. The resulting +// plaintext is printed to STDOUT. +// +// Returns 0 on success, or non-zero on failure after printing error +// messages to stderr. +int do_get_request(const demo_client_request_options *options); + +// State related to a demo HTTPS client connection. +typedef struct demo_client_connection +{ + // the options used to create the connection. + const demo_client_request_options *options; + // the socket file descriptor for the connection. + int sockfd; + // the rustls_connection for TLS. + rustls_connection *rconn; + // the connection data for the rustls_connection. + conndata *data; + // whether the connection is closing + bool closing; + // whether the connection was closed cleanly. + bool clean_closure; +} demo_client_connection; + +// Free a demo_client_connection. +void demo_client_connection_free(demo_client_connection *conn); + +// Create a new demo_client_connection by connecting to the server +// specified by the options. +// +// The caller owns the resulting demo_client_connection and must free it +// with demo_client_connection_free. The provided +// options must outlive the connection. +// +// Returns NULL on failure after printing error messages to stderr. +demo_client_connection *demo_client_connect( + const demo_client_request_options *options); + +// Number of attempts to make in connect_socket. +#define MAX_CONNECT_ATTEMPTS 10 + +// Connect a socket to the hostname/port specified in the +// demo_client_request_options. Tries up to MAX_CONNECT_ATTEMPTS times before +// giving up. +// +// Returns a non-zero FD for the connected socket if successful, or 0 on +// failure after printing error messages to stderr. +int connect_socket(const demo_client_request_options *options); + +// Write a GET request to the provided demo_client_connection. +int demo_client_connection_write_get(const demo_client_connection *demo_conn); + +// Read TLS data from the provided demo_client_connection's socket, updating +// the rustls connection and putting any available plaintext in the demo +// connection's data. +// +// Returns a demo_result indicating the result of the read. +demo_result demo_client_connection_read_tls(demo_client_connection *demo_conn); + +// Write queued TLS data to the provided demo_client_connection's socket. +// +// Returns a demo_result indicating the result of the write. +demo_result demo_client_connection_write_tls( + const demo_client_connection *demo_conn); + +// A callback for rustls certificate validation that *unsafely* allows all +// presented certificate chains, printing their contents to stderr. +uint32_t unsafe_skip_verify(void *userdata, + const rustls_verify_server_cert_params *params); + +#endif // CLIENT_H diff --git a/tests/server.c b/tests/server.c index cd058329..28b5ae27 100644 --- a/tests/server.c +++ b/tests/server.c @@ -22,6 +22,7 @@ /* rustls.h is autogenerated in the Makefile using cbindgen. */ #include "rustls.h" #include "common.h" +#include "server.h" typedef enum exchange_state { @@ -83,6 +84,12 @@ handle_conn(conndata *conn) FD_SET(sockfd, &write_fds); } + if(state == SENT_RESPONSE && !rustls_connection_wants_write(rconn)) { + LOG_SIMPLE( + "sent response and rustls doesn't want write. closing connection"); + goto cleanup; + } + if(!rustls_connection_wants_read(rconn) && !rustls_connection_wants_write(rconn)) { LOG_SIMPLE("rustls wants neither read nor write. closing connection"); @@ -117,6 +124,10 @@ handle_conn(conndata *conn) } if(FD_ISSET(sockfd, &write_fds)) { const rustls_io_result err = write_tls(rconn, conn, &n); + if(err == EAGAIN) { + LOG_SIMPLE("writing to socket: EAGAIN or EWOULDBLOCK"); + continue; + } if(err != 0) { LOG("error in write_tls: errno %d", err); goto cleanup; diff --git a/tests/server.h b/tests/server.h new file mode 100644 index 00000000..44c4a867 --- /dev/null +++ b/tests/server.h @@ -0,0 +1,7 @@ +#ifndef SERVER_H +#define SERVER_H + +// Placeholder so that Makefile compilation rule for client/server +// can remain symmetric. + +#endif // SERVER_H