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(u: T, cfg: tokio_rustls::rustls::ClientConfig) -> Result where T: Into, { 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 = 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 { 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(()) } }