dope-tls 0.1.1

TCP TLS streams for dope runtime, shin-backed
Documentation

use std::cell::RefCell;
use std::future::Future;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::pin::pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};

use dope_futures::{AsyncRead, AsyncWrite};
use dope_tls::future::TlsConnector;

struct BlockingIo {
    sock: Rc<RefCell<TcpStream>>,
}

impl AsyncRead for BlockingIo {
    async fn read(&mut self, mut buf: Vec<u8>) -> (std::io::Result<usize>, Vec<u8>) {
        let cap = buf.capacity().max(buf.len()).max(1);
        buf.resize(cap, 0);
        let res = self.sock.borrow_mut().read(&mut buf);
        match res {
            Ok(n) => {
                buf.truncate(n);
                (Ok(n), buf)
            }
            Err(e) => (Err(e), buf),
        }
    }
}

impl AsyncWrite for BlockingIo {
    async fn write(&mut self, buf: Vec<u8>) -> (std::io::Result<usize>, Vec<u8>) {
        let res = self.sock.borrow_mut().write_all(&buf).map(|_| buf.len());
        (res, buf)
    }
}

fn block_on<F: Future>(f: F) -> F::Output {
    let mut f = pin!(f);
    let waker = Waker::noop();
    let mut cx = Context::from_waker(waker);
    loop {
        if let Poll::Ready(v) = f.as_mut().poll(&mut cx) {
            return v;
        }
    }
}

#[test]
#[ignore]
fn live_https_get_example_com() {
    const HOST: &str = "example.com";
    const PORT: u16 = 443;

    let sock = TcpStream::connect((HOST, PORT)).expect("tcp connect");
    sock.set_nodelay(true).ok();
    let io = BlockingIo {
        sock: Rc::new(RefCell::new(sock)),
    };

    let connector = TlsConnector::from_webpki_roots()
        .with_alpn(vec![b"http/1.1".to_vec()]);

    let mut tls = block_on(async { connector.connect_hostname(HOST, io).await })
        .expect("tls handshake");

    if let Some(picked) = tls.selected_alpn() {
        assert_eq!(picked, b"http/1.1", "server picked unsupported ALPN");
    }

    let req = format!(
        "GET / HTTP/1.1\r\nHost: {HOST}\r\nUser-Agent: shin-test\r\nConnection: close\r\nAccept: */*\r\n\r\n"
    );

    let (res, _) = block_on(tls.write(req.into_bytes()));
    res.expect("tls write");

    let mut response = Vec::new();
    loop {
        let (res, buf) = block_on(tls.read(vec![0u8; 8192]));
        match res {
            Ok(0) => break,
            Ok(n) => response.extend_from_slice(&buf[..n]),
            Err(e) => panic!("tls read: {e:?}"),
        }
        if response.len() > 256 * 1024 {
            break;
        }
    }

    let head_end = response.windows(4).position(|w| w == b"\r\n\r\n").expect(
        "found end of headers in response",
    );
    let head = std::str::from_utf8(&response[..head_end]).expect("ascii head");
    let status_line = head.lines().next().expect("status line");
    println!("status: {status_line}");
    assert!(
        status_line.starts_with("HTTP/1.1 "),
        "unexpected status: {status_line}",
    );
    let code: u16 = status_line
        .split_whitespace()
        .nth(1)
        .and_then(|s| s.parse().ok())
        .expect("parse status code");
    assert!(
        (200..400).contains(&code),
        "expected 2xx/3xx, got {code}: {status_line}",
    );

    block_on(async { tls.shutdown().await }).expect("tls shutdown");
}