maj 0.6.0

A gemini client and server for Rust
Documentation
use crate::Response;
use std::{io::Cursor, sync::Arc};
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpStream,
};
use tokio_rustls::{rustls::TLSError, TlsConnector};
use url::Url;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("TLS error: {0:?}")]
    TLS(#[from] TLSError),

    #[error("URL error: {0:?}")]
    URL(#[from] url::ParseError),

    #[error("Invalid DNS name: {0:?}")]
    InvalidDNSName(#[from] webpki::InvalidDNSNameError),

    #[error("IO error: {0:?}")]
    IO(#[from] std::io::Error),

    #[error("Response parsing error: {0:?}")]
    ResponseParse(#[from] crate::ResponseError),

    #[error("Invalid URL scheme {0:?}")]
    InvalidScheme(String),
}

pub async fn get<T>(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result<crate::Response, Error>
where
    T: Into<String>,
{
    let u = u.into();
    let mut ur = Url::parse(&u.clone())?;
    if ur.port().is_none() {
        ur.set_port(Some(1965)).unwrap();
    }

    if ur.scheme() != "gemini" {
        return Err(Error::InvalidScheme(ur.scheme().to_string()));
    }

    let cfg = Arc::new(cfg);
    let host = ur.host_str().unwrap();
    let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?;
    let config = TlsConnector::from(cfg);

    let sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap())).await?;
    let mut tls = config.connect(name_ref, sock).await?;

    let req = format!("{}\r\n", u);
    log::trace!("writing request {:?}", req);
    tls.write(req.as_bytes()).await?;
    let mut buf: Vec<u8> = vec![];
    tls.read_to_end(&mut buf).await?;
    Ok(Response::parse(&mut Cursor::new(buf))?)
}

#[cfg(test)]
mod tests {
    use tokio_rustls::rustls;

    fn config() -> rustls::ClientConfig {
        let mut config = rustls::ClientConfig::new();
        config
            .dangerous()
            .set_certificate_verifier(Arc::new(NoCertificateVerification {}));

        config
    }

    struct NoCertificateVerification {}

    impl rustls::ServerCertVerifier for NoCertificateVerification {
        fn verify_server_cert(
            &self,
            _roots: &rustls::RootCertStore,
            _presented_certs: &[rustls::Certificate],
            _dns_name: webpki::DNSNameRef<'_>,
            _ocsp: &[u8],
        ) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
            Ok(rustls::ServerCertVerified::assertion())
        }
    }

    use super::*;

    #[tokio::test]
    async fn gemini_homepage() -> Result<(), Error> {
        let _ = pretty_env_logger::try_init();
        let resp = get("gemini://gemini.circumlunar.space/".to_string(), config()).await?;

        assert_eq!(resp.status, crate::StatusCode::Success);
        assert_eq!(resp.meta, "text/gemini");
        assert_ne!(resp.body.len(), 0);

        Ok(())
    }

    #[tokio::test]
    async fn gus() -> Result<(), Error> {
        let _ = pretty_env_logger::try_init();
        let resp = get("gemini://gus.guru/".to_string(), config()).await?;

        assert_eq!(resp.status, crate::StatusCode::Success);
        assert_eq!(resp.meta, "text/gemini");
        assert_ne!(resp.body.len(), 0);

        Ok(())
    }
}