diff --git a/Cargo.toml b/Cargo.toml index 186a743..d9f2e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ num-derive = "0.3" num-traits = "0.2" rustls = "0.18" webpki = "0.21.0" +webpki-roots = "0.20" log = "0.4" url = "2" thiserror = "1" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..4cba5e5 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,64 @@ +use crate::{Response, StatusCode}; +use rustls::{ClientConfig, ClientSession, Stream, TLSError}; +use std::{io::prelude::*, net::TcpStream, sync::Arc}; +use url::Url; + +fn config() -> ClientConfig { + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + config +} + +#[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), +} + +pub fn get(u: String) -> Result { + let mut ur = Url::parse(&u.clone())?; + if ur.port().is_none() { + ur.set_port(Some(1965)).unwrap(); + } + let cfg = Arc::new(config()); + let host = ur.host_str().unwrap(); + let mut sock = TcpStream::connect(&format!("{}:{}", host, ur.port().unwrap()))?; + let name_ref = webpki::DNSNameRef::try_from_ascii_str(host)?; + let mut client = ClientSession::new(&cfg, name_ref); + let mut tls = Stream::new(&mut client, &mut sock); + + let req = format!("{}\r\n", u); + log::trace!("writing request {:?}", req); + tls.write(req.as_bytes())?; + Ok(Response::parse(&mut tls)?) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn gemini_homepage() -> Result<(), Error> { + let _ = pretty_env_logger::try_init(); + let resp = get("gemini://gemini.circumlunar.space/".to_string())?; + + assert_eq!(resp.status, StatusCode::Success); + assert_eq!(resp.meta, "text/gemini"); + assert_ne!(resp.body.len(), 0); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6878d34..ef4c3f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ +mod client; mod status_code; mod response; pub use status_code::StatusCode; pub use response::{Response, Error as ResponseError}; +pub use client::{get, Error as ClientError}; diff --git a/src/response.rs b/src/response.rs index 08319e5..c55c2da 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,13 +1,13 @@ use crate::StatusCode; use num::FromPrimitive; -use std::io::prelude::*; +use std::io::{prelude::*, ErrorKind}; /// A Gemini response as specified in [the spec](https://gemini.circumlunar.space/docs/specification.html). #[derive(Default)] pub struct Response { - status: StatusCode, - meta: String, - body: Vec, + pub status: StatusCode, + pub meta: String, + pub body: Vec, } /// The parser state. @@ -39,6 +39,9 @@ pub enum Error { #[error("None found when none should not be found")] NoneFound, + + #[error("TLS error: {0:?}")] + TLS(#[from] rustls::TLSError), } impl Response { @@ -57,8 +60,19 @@ impl Response { } } log::trace!("buf: {:?}: {:?}", buf, buf[0] as char); + log::trace!("n: {}", n); + } + + Err(why) => { + if why.kind() == ErrorKind::ConnectionAborted { + if let State::ReadBody { data } = state { + result.body = data; + return Ok(result); + } + } + + return Err(Error::IO(why)); } - Err(why) => return Err(Error::IO(why)), } // log::trace!("state: {:?}", state);