diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..25b13b9a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crustls" +version = "0.1.0" +authors = ["Jacob Hoffman-Andrews "] +description = "C-to-rustls bindings" +edition = "2018" + +[dependencies] +rustls = "0.19" +webpki = "0.21" +libc = "0.2" +env_logger = "0.8" +webpki-roots = "0.21" + +[dev_dependencies] +cbindgen = "*" + +[lib] +name = "crustls" +crate-type = ["staticlib"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..56967714 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +CFLAGS := -Werror -Wall -Wextra -Wpedantic +LDFLAGS := -Wl,--gc-sections -lpthread -ldl + +all: target/crustls-demo + target/crustls-demo httpbin.org /headers + +target: + mkdir -p $@ + +src/lib.h: src/lib.rs + cbindgen --lang C --output src/lib.h + +target/crustls-demo: target/main.o target/debug/libcrustls.a + $(CC) -o $@ $^ $(LDFLAGS) + +target/debug/libcrustls.a: src/lib.rs Cargo.toml + cargo build + +target/main.o: src/main.c src/lib.h | target + $(CC) -o $@ -c $< $(CFLAGS) + +clean: + rm -rf target diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..a23c70e1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,337 @@ +#![crate_type = "staticlib"] + +use libc::{c_char, c_int, c_void, size_t, ssize_t}; +use std::io::{Cursor, Read, Write}; +use std::slice; +use std::{ffi::CStr, sync::Arc}; +use std::{io::ErrorKind::ConnectionAborted, mem}; + +use rustls::{ClientConfig, ClientSession, Session}; + +type CrustlsResult = c_int; + +pub const CRUSTLS_OK: c_int = 0; +pub const CRUSTLS_ERROR: c_int = 1; + +/// Create a client_config. Caller owns the memory and must free it with +/// rustls_client_config_free. +#[no_mangle] +pub extern "C" fn rustls_client_config_new() -> *const c_void { + let mut config = rustls::ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + env_logger::init(); + Arc::into_raw(Arc::new(config)) as *const c_void +} + +/// "Free" a client_config previously returned from rustls_client_config_new. +/// Since client_config is actually an atomically reference-counted pointer, +/// extant client_sessions may still hold an internal reference to the +/// Rust object. However, C code must consider this pointer unusable after +/// "free"ing it. +/// Calling with NULL is fine. Must not be called twice with the same value. +#[no_mangle] +pub extern "C" fn rustls_client_config_free(config: *const c_void) { + unsafe { + if let Some(c) = (config as *const ClientConfig).as_ref() { + // To free the client_config, we reconstruct the Arc. It should have a refcount of 1, + // representing the C code's copy. When it drops, that refcount will go down to 0 + // and the inner ClientConfig will be dropped. + let arc: Arc = Arc::from_raw(c); + let strong_count = Arc::strong_count(&arc); + if strong_count < 1 { + eprintln!( + "rustls_client_config_free: invariant failed: arc.strong_count was < 1: {}. \ + You must not free the same client_config multiple times.", + strong_count + ); + } + } else { + eprintln!("rustls_client_config_free: config was NULL"); + } + }; +} + +/// In rustls_client_config_new, we create an Arc, then call `into_raw` and return the resulting raw +/// pointer to C. C can then call rustls_client_session_new multiple times using that same raw +/// pointer. On each call, we need to reconstruct the Arc. But once we reconstruct the Arc, its +/// reference count will be decremented on drop. We need to reference count to stay at 1, because +/// the C code is holding a copy. This function turns the raw pointer back into an Arc, clones it +/// to increment the reference count (which will make it 2 in this particular case), and +/// mem::forgets the clone. The mem::forget prevents the reference count from being decremented when +/// we exit this function, so it will stay at 2 as long as we are in Rust code. Once the caller +/// drops its Arc, the reference count will go back down to 1, indicating the C code's copy. +/// +/// Unsafety: +/// +/// v must be a non-null pointer that resulted from previously calling `Arc::into_raw`. +unsafe fn arc_with_incref_from_raw(v: *const T) -> Arc { + let r = Arc::from_raw(v); + let val = Arc::clone(&r); + mem::forget(r); + val +} + +/// Create a new rustls::ClientSession, and return it in the output parameter `out`. +/// If this returns an error code, the memory pointed to by `session_out` remains unchanged. +/// If this returns a non-error, the memory pointed to by `session_out` is modified to point +/// at a valid ClientSession. The caller now owns the ClientSession and must call +/// `rustls_client_session_free` when done with it. +#[no_mangle] +pub extern "C" fn rustls_client_session_new( + config: *const c_void, + hostname: *const c_char, + session_out: *mut *mut c_void, +) -> CrustlsResult { + let hostname: &CStr = unsafe { + if hostname.is_null() { + eprintln!("rustls_client_session_new: hostname was NULL"); + return CRUSTLS_ERROR; + } + CStr::from_ptr(hostname) + }; + let config: Arc = unsafe { + match (config as *const ClientConfig).as_ref() { + Some(c) => arc_with_incref_from_raw(c), + None => { + eprintln!("rustls_client_session_new: config was NULL"); + return CRUSTLS_ERROR; + } + } + }; + let hostname: &str = match hostname.to_str() { + Ok(s) => s, + Err(e) => { + eprintln!("converting hostname to Rust &str: {}", e); + return CRUSTLS_ERROR; + } + }; + let name_ref = match webpki::DNSNameRef::try_from_ascii_str(hostname) { + Ok(nr) => nr, + Err(e) => { + eprintln!( + "turning hostname '{}' into webpki::DNSNameRef: {}", + hostname, e + ); + return CRUSTLS_ERROR; + } + }; + let client = ClientSession::new(&config, name_ref); + + // We've succeeded. Put the client on the heap, and transfer ownership + // to the caller. After this point, we must return CRUSTLS_OK so the + // caller knows it is responsible for this memory. + let b = Box::new(client); + unsafe { + *session_out = Box::into_raw(b) as *mut c_void; + } + + return CRUSTLS_OK; +} + +#[no_mangle] +pub extern "C" fn rustls_client_session_wants_read(session: *const c_void) -> bool { + unsafe { + match (session as *const ClientSession).as_ref() { + Some(cs) => cs.wants_read(), + None => false, + } + } +} + +#[no_mangle] +pub extern "C" fn rustls_client_session_wants_write(session: *const c_void) -> bool { + unsafe { + match (session as *const ClientSession).as_ref() { + Some(cs) => cs.wants_write(), + None => false, + } + } +} + +#[no_mangle] +pub extern "C" fn rustls_client_session_process_new_packets(session: *mut c_void) -> CrustlsResult { + let session: &mut ClientSession = unsafe { + match (session as *mut ClientSession).as_mut() { + Some(cs) => cs, + None => { + eprintln!("ClientSession::process_new_packets: session was NULL"); + return CRUSTLS_ERROR; + } + } + }; + let result: CrustlsResult = match session.process_new_packets() { + Ok(()) => CRUSTLS_OK, + Err(e) => { + eprintln!("ClientSession::process_new_packets: {}", e); + CRUSTLS_ERROR + } + }; + result +} + +/// Free a client_session previously returned from rustls_client_session_new. +/// Calling with NULL is fine. Must not be called twice with the same value. +#[no_mangle] +pub extern "C" fn rustls_client_session_free(session: *mut c_void) { + unsafe { + if let Some(c) = (session as *mut ClientSession).as_mut() { + // Convert the pointer to a Box and drop it. + Box::from_raw(c); + } else { + eprintln!("warning: rustls_client_config_free: config was NULL"); + } + } +} + +/// Write plaintext bytes into the ClientSession. This acts like +/// write(2). It returns the number of bytes written, or -1 on error. +#[no_mangle] +pub extern "C" fn rustls_client_session_write( + session: *const c_void, + buf: *const u8, + count: size_t, +) -> ssize_t { + let session: &mut ClientSession = unsafe { + match (session as *mut ClientSession).as_mut() { + Some(cs) => cs, + None => { + eprintln!("ClientSession::write: session was NULL"); + return -1; + } + } + }; + let write_buf: &[u8] = unsafe { + if buf.is_null() { + eprintln!("ClientSession::write: buf was NULL"); + return -1; + } + slice::from_raw_parts(buf, count as usize) + }; + let n_written: usize = match session.write(write_buf) { + Ok(n) => n, + Err(e) => { + eprintln!("ClientSession::write: {}", e); + return -1; + } + }; + n_written as ssize_t +} + +/// Read plaintext bytes from the ClientSession. This acts like +/// read(2), writing the plaintext bytes into `buf`. It returns +/// the number of bytes read, or -1 on error. +#[no_mangle] +pub extern "C" fn rustls_client_session_read( + session: *const c_void, + buf: *mut u8, + count: size_t, +) -> ssize_t { + let session: &mut ClientSession = unsafe { + match (session as *mut ClientSession).as_mut() { + Some(cs) => cs, + None => { + eprintln!("ClientSession::read: session was NULL"); + return -1; + } + } + }; + let read_buf: &mut [u8] = unsafe { + if buf.is_null() { + eprintln!("ClientSession::read: buf was NULL"); + return -1; + } + slice::from_raw_parts_mut(buf, count as usize) + }; + // Since it's *possible* for a Read impl to consume the possibly-uninitialized memory from buf, + // zero it out just in case. TODO: use Initializer once it's stabilized. + // https://doc.rust-lang.org/nightly/std/io/trait.Read.html#method.initializer + for c in read_buf.iter_mut() { + *c = 0; + } + let n_read: usize = match session.read(read_buf) { + Ok(n) => n, + // The CloseNotify TLS alert is benign, but rustls returns it as an Error. See comment on + // https://docs.rs/rustls/0.19.0/rustls/struct.ClientSession.html#impl-Read. + // Log it and return EOF. + Err(e) if e.kind() == ConnectionAborted && e.to_string().contains("CloseNotify") => { + eprintln!("ClientSession::read: CloseNotify (this is expected): {}", e); + return 0; + } + Err(e) => { + eprintln!("ClientSession::read: {}", e); + return -1; + } + }; + n_read as ssize_t +} + +/// Read TLS bytes taken from a socket into the ClientSession. This acts like +/// read(2). It returns the number of bytes read, or -1 on error. +#[no_mangle] +pub extern "C" fn rustls_client_session_read_tls( + session: *const c_void, + buf: *const u8, + count: size_t, +) -> ssize_t { + let session: &mut ClientSession = unsafe { + match (session as *mut ClientSession).as_mut() { + Some(cs) => cs, + None => { + eprintln!("ClientSession::read_tls: session was NULL"); + return -1; + } + } + }; + let input_buf: &[u8] = unsafe { + if buf.is_null() { + eprintln!("ClientSession::read_tls: buf was NULL"); + return -1; + } + slice::from_raw_parts(buf, count as usize) + }; + let mut cursor = Cursor::new(input_buf); + let n_read: usize = match session.read_tls(&mut cursor) { + Ok(n) => n, + Err(e) => { + eprintln!("ClientSession::read_tls: {}", e); + return -1; + } + }; + n_read as ssize_t +} + +/// Write TLS bytes from the ClientSession into a buffer. Those bytes should then be written to +/// a socket. This acts like write(2). It returns the number of bytes read, or -1 on error. +#[no_mangle] +pub extern "C" fn rustls_client_session_write_tls( + session: *const c_void, + buf: *mut u8, + count: size_t, +) -> ssize_t { + let session: &mut ClientSession = unsafe { + match (session as *mut ClientSession).as_mut() { + Some(cs) => cs, + None => { + eprintln!("ClientSession::write_tls: session was NULL"); + return -1; + } + } + }; + let mut output_buf: &mut [u8] = unsafe { + if buf.is_null() { + eprintln!("ClientSession::write_tls: buf was NULL"); + return -1; + } + slice::from_raw_parts_mut(buf, count as usize) + }; + let n_written: usize = match session.write_tls(&mut output_buf) { + Ok(n) => n, + Err(e) => { + eprintln!("ClientSession::write_tls: {}", e); + return -1; + } + }; + n_written as ssize_t +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 00000000..47a6c472 --- /dev/null +++ b/src/main.c @@ -0,0 +1,295 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* lib.h is autogenerated in the Makefile using cbindgen. */ +#include "lib.h" + +/* + * Write n bytes from buf to the provided fd, retrying short writes until + * we finish or hit an error. Assumes fd is blocking and therefore doesn't + * handle EAGAIN. Returns 0 for success or 1 for error. + */ +int +write_all(int fd, const char *buf, int n) +{ + int m = 0; + while(n > 0) { + m = write(fd, buf, n); + if(m < 0) { + perror("writing to stdout"); + return 1; + } + if(m == 0) { + fprintf(stderr, "early EOF when writing to stdout\n"); + return 1; + } + n -= m; + } + return 0; +} + +/* + * Connect to the given hostname on port 443 and return the file descriptor of + * the socket. On error, print the error and return 1. Caller is responsible + * for closing socket. + */ +int +make_conn(const char *hostname) +{ + struct addrinfo *getaddrinfo_output = NULL; + int getaddrinfo_result = + getaddrinfo(hostname, "443", NULL, &getaddrinfo_output); + if(getaddrinfo_result != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(getaddrinfo_result)); + goto cleanup; + } + + int sockfd = socket(getaddrinfo_output->ai_family, + getaddrinfo_output->ai_socktype, + getaddrinfo_output->ai_protocol); + if(sockfd < 0) { + perror("making socket"); + goto cleanup; + } + + int connect_result = connect( + sockfd, getaddrinfo_output->ai_addr, getaddrinfo_output->ai_addrlen); + if(connect_result < 0) { + perror("connecting"); + goto cleanup; + } + + return sockfd; + +cleanup: + if(getaddrinfo_output != NULL) { + freeaddrinfo(getaddrinfo_output); + } + if(sockfd > 0) { + close(sockfd); + } + return -1; +} + +/* + * Given an established TCP connection, and a rustls client_session, 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(int sockfd, void *client_session, + const char *hostname, const char *path) +{ + int ret = 1; + int result = 1; + char buf[2048]; + + bzero(buf, sizeof(buf)); + snprintf(buf, + sizeof(buf), + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: crustls-demo\r\n" + "Accept: carcinization/inevitable, text/html\r\n" + "Connection: close\r\n" + "\r\n", + path, + hostname); + int n = + rustls_client_session_write(client_session, (uint8_t *)buf, strlen(buf)); + if(n < 0) { + fprintf(stderr, "error writing plaintext bytes to ClientSession\n"); + goto cleanup; + } + +#define MAX_EVENTS 1 + struct epoll_event ev, events[MAX_EVENTS]; + int nfds = 0; + int epollfd = epoll_create1(0); + if(epollfd == -1) { + perror("epoll_create1"); + goto cleanup; + } + + ev.events = EPOLLIN | EPOLLOUT; + ev.data.fd = sockfd; + if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) { + perror("epoll_ctl: listen_sock"); + goto cleanup; + } + + for(;;) { + nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); + if(nfds == -1) { + perror("epoll_wait"); + goto cleanup; + } + + if(rustls_client_session_wants_read(client_session) && + (events[0].events & EPOLLIN) > 0) { + fprintf(stderr, + "ClientSession wants us to read_tls. First we need to pull some " + "bytes from the socket\n"); + + bzero(buf, sizeof(buf)); + n = read(sockfd, buf, sizeof(buf)); + if(n == 0) { + fprintf(stderr, "EOF reading from socket\n"); + break; + } + else if(n < 0) { + perror("reading from socket"); + goto cleanup; + } + fprintf(stderr, "read %d bytes from socket\n", n); + + /* + * Now pull those bytes from the buffer into ClientSession. + * Note that we pass buf, n; not buf, sizeof(buf). We don't + * want to pull in unitialized memory that we didn't just + * read from the socket. + */ + n = rustls_client_session_read_tls(client_session, (uint8_t *)buf, n); + if(n == 0) { + fprintf(stderr, "EOF from ClientSession::read_tls\n"); + // TODO: What to do here? + break; + } + else if(n < 0) { + fprintf(stderr, "Error in ClientSession::read_tls\n"); + goto cleanup; + } + + result = rustls_client_session_process_new_packets(client_session); + if(result != CRUSTLS_OK) { + fprintf(stderr, "Error in process_new_packets"); + goto cleanup; + } + + /* Read all available bytes from the client_session until EOF. + * Note that EOF here indicates "no more bytes until + * process_new_packets", not "stream is closed". + */ + for(;;) { + bzero(buf, sizeof(buf)); + n = rustls_client_session_read( + client_session, (uint8_t *)buf, sizeof(buf)); + if(n == 0) { + fprintf(stderr, "EOF from ClientSession::read (this is expected)\n"); + break; + } + else if(n < 0) { + fprintf(stderr, "Error in ClientSession::read\n"); + goto cleanup; + } + + result = write_all(STDOUT_FILENO, buf, n); + if(result != 0) { + goto cleanup; + } + } + } + if(rustls_client_session_wants_write(client_session) && + (events[0].events & EPOLLOUT) > 0) { + fprintf(stderr, "ClientSession wants us to write_tls.\n"); + bzero(buf, sizeof(buf)); + n = rustls_client_session_write_tls( + client_session, (uint8_t *)buf, sizeof(buf)); + if(n == 0) { + fprintf(stderr, "EOF from ClientSession::write_tls\n"); + goto cleanup; + } + else if(n < 0) { + fprintf(stderr, "Error in ClientSession::write_tls\n"); + goto cleanup; + } + + result = write_all(sockfd, buf, n); + if(result != 0) { + goto cleanup; + } + } + } + + ret = 0; +cleanup: + if(epollfd > 0) { + close(epollfd); + } + return ret; +} + +int +do_request(const void *client_config, const char *hostname, const char *path) +{ + int ret = 1; + int sockfd = make_conn(hostname); + if(sockfd < 0) { + // No perror because make_conn printed error already. + goto cleanup; + } + + void *client_session = NULL; + int result = + rustls_client_session_new(client_config, hostname, &client_session); + if(result != CRUSTLS_OK) { + goto cleanup; + } + + ret = send_request_and_read_response(sockfd, client_session, hostname, path); + if(ret != CRUSTLS_OK) { + goto cleanup; + } + + ret = 0; + +cleanup: + rustls_client_session_free(client_session); + if(sockfd > 0) { + close(sockfd); + } + return ret; +} + +int +main(int argc, const char **argv) +{ + int ret = 1; + int result = 1; + if(argc <= 2) { + fprintf(stderr, + "usage: %s hostname path\n\n" + "Connect to a host via HTTPS on port 443, make a request for the\n" + "given path, and emit response to stdout.\n", + argv[0]); + return 1; + } + const char *hostname = argv[1]; + const char *path = argv[2]; + + const void *client_config = rustls_client_config_new(); + + int i; + for(i = 0; i < 3; i++) { + result = do_request(client_config, hostname, path); + if(result != 0) { + goto cleanup; + } + } + + // Success! + ret = 0; + +cleanup: + rustls_client_config_free(client_config); + return ret; +}