cinema 0.1.0

HTTP record-replay proxy for Rust tests
Documentation
use crate::prelude::*;
use hyper::body::Incoming;
use hyper_util::rt::TokioIo;
use std::pin::Pin;
use std::str::FromStr;
use tokio::net::TcpStream;
use tokio::net::lookup_host;
use tokio_native_tls::{TlsConnector, native_tls};

type BoxedHttpConnection =
    Pin<Box<dyn std::future::Future<Output = Result<(), hyper::Error>> + Send>>;

pub struct ProxyForwardRequestPayload<'a> {
    pub uri: Uri,
    pub method: &'a Method,
    pub headers: &'a HeaderMap,
    pub body: Option<Bytes>,
}

impl Proxy {
    pub(crate) async fn forward_request(
        payload: ProxyForwardRequestPayload<'_>,
        origin: String,
    ) -> Result<(Request<Full<Bytes>>, Response<Incoming>), CinemaError> {
        let origin_uri = Uri::from_str(&origin).unwrap();
        let host = origin_uri.host().unwrap();
        let is_https = origin_uri.scheme_str() == Some("https");
        let port = origin_uri
            .port_u16()
            .unwrap_or(if is_https { 443 } else { 80 });
        let lookup_for = format!("{}:{}", host, port);
        let addr = lookup_host(&lookup_for)
            .await
            .map_err(|err| CinemaError::LookupHost(err, lookup_for.to_string()))?
            // [TODO] Handle multiple addresses
            .next()
            .unwrap();
        let stream = TcpStream::connect(&addr)
            .await
            .map_err(|err| CinemaError::ConnectTcpStream(err, addr.to_string()))?;

        let mut sender;
        let conn_future: BoxedHttpConnection = if is_https {
            let native_tls = native_tls::TlsConnector::new()?;
            let tls_connector = TlsConnector::from(native_tls);

            let tls_stream = tls_connector.connect(host, stream).await?;

            let io = TokioIo::new(tls_stream);
            let (s, conn) = hyper::client::conn::http1::handshake(io)
                .await
                .map_err(|err| CinemaError::Handshake(err, origin.clone()))?;

            sender = s;
            Box::pin(conn)
        } else {
            let io = TokioIo::new(stream);
            let (s, conn) = hyper::client::conn::http1::handshake(io)
                .await
                .map_err(|err| CinemaError::Handshake(err, origin.clone()))?;

            sender = s;
            Box::pin(conn)
        };

        tokio::task::spawn(async move {
            if let Err(err) = conn_future.await {
                // [TODO] Handle the error
                println!("Connection failed: {:?}", err);
            }
        });

        let mut req_builder = Request::builder().method(payload.method).uri(payload.uri);

        req_builder = req_builder
            .header(hyper::header::USER_AGENT, "cinema/0.1")
            .header(hyper::header::HOST, host);

        for (name, value) in payload.headers.iter() {
            match *name {
                // Skip headers that we set manually
                hyper::header::HOST | hyper::header::USER_AGENT => {}

                _ => {
                    req_builder = req_builder.header(name, value);
                }
            }
        }

        let forward_req = req_builder
            .body(Full::new(match payload.body {
                Some(body) => body,
                None => Bytes::new(),
            }))
            .map_err(|err| CinemaError::ForwardBody(err))?;

        let res = sender
            .send_request(forward_req.clone())
            .await
            .map_err(|err| CinemaError::ForwardRequest(err))?;

        Ok((forward_req, res))
    }
}