diff --git a/src/client/mod.rs b/src/client/mod.rs index 93abe81..74c0ba3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -49,19 +49,55 @@ impl Client { } impl Client { - /// Parse the given URL, do some checks, - /// take host and port (default is 1965) from the URL - /// and perform a Gemini request, returning [`Response`] on success. + /// Perform a Gemini request with the specified URL. + /// Host and port (1965 by default) are parsed from `url_str` + /// after scheme and userinfo checks. + /// On success, [`Response`] is returned. + /// + /// Automatically follows redirections up to 5 times. + /// To avoid this behavior, use [`Client::request_with_no_redirect`], + /// this function is also called here under the hood. + /// + /// Returns an error if a scheme is not `gemini://` or + /// a userinfo portion (`user:password@`) is present in the URL. + /// To avoid this checks, most probably for proxying requests, + /// use [`Client::request_with_host`]. /// /// # Errors - /// Everything is converted into [`LibError`]. - /// - [`InvalidUrl::ParseError`] means that the given URL cannot be parsed. - /// - [`InvalidUrl::SchemeNotGemini`] is returned when the scheme is not `gemini://` - /// (for proxying use [`Client::request_with_host`]). - /// - [`InvalidUrl::UserinfoPresent`] is returned when the URL contains userinfo -- `user:password@` -- - /// which is forbidden by the Gemini specs. - /// - For the rest, see [`Client::request_with_host`]. + /// - See [`Client::request_with_no_redirect`]. pub async fn request(&self, url_str: &str) -> Result { + // first request + let mut resp = self.request_with_no_redirect(url_str).await?; + + let mut i: u8 = 0; + const MAX: u8 = 5; + + // repeat requests until we get a non-30 status + // or hit the redirection depth limit + loop { + if resp.status().reply_type() == ReplyType::Redirect && i < MAX { + resp = self.request_with_no_redirect(resp.message()).await?; + i += 1; + continue; + } + return Ok(resp); + } + } + + /// Perform a Gemini request with the specified URL + /// **without** following redirections. + /// Host and port (1965 by default) are parsed from `url_str` + /// after scheme and userinfo checks. + /// On success, [`Response`] is returned. + /// + /// # Errors + /// - [`InvalidUrl::ParseError`] means that the given URL cannot be parsed. + /// - [`InvalidUrl::SchemeNotGemini`] is returned when a scheme is not `gemini://`, + /// for proxying requests use [`Client::request_with_host`]. + /// - [`InvalidUrl::UserinfoPresent`] is returned when the given URL contains + /// a userinfo portion (`user:password@`) -- it is forbidden by the Gemini specification. + /// - See [`Client::request_with_host`] for the rest. + pub async fn request_with_no_redirect(&self, url_str: &str) -> Result { let url = Url::parse(url_str).map_err(InvalidUrl::ParseError)?; // deny non-Gemini requests if url.scheme() != "gemini" { @@ -82,17 +118,22 @@ impl Client { /// Non-`gemini://` URLs is OK if the remote server supports proxying. /// /// # Errors - /// Everything is converted into [`LibError`]. - /// - [`InvalidUrl::ConvertError`] is an error while converting - /// host and port into [`std::net::SocketAddr`] or [`pki_types::ServerName`]. - /// - [`std::io::Error`] is returned in nearly all cases: + /// - [`InvalidUrl::ConvertError`] means that a hostname cannot be + /// converted into [`pki_types::ServerName`]. + /// - [`LibError::HostLookupError`] means that a DNS server returned no records, + /// i. e. that domain does not exist. + /// - [`LibError::DnsClientError`] (crate feature `hickory`) + /// wraps a Hickory DNS client error related to a connection failure + /// or an invalid DNS server response. + /// - [`std::io::Error`] is returned in many cases: /// could not open a TCP connection, perform a TLS handshake, /// write to or read from the TCP stream. - /// Check the inner error if you need to determine what exactly happened. - /// - [`LibError::StatusOutOfRange`] means that a server returned an incorrect status code - /// (less than 10 or greater than 69). + /// Check the ErrorKind and/or the inner error + /// if you need to determine what exactly happened. + /// - [`LibError::StatusOutOfRange`] means that a Gemini server returned + /// an invalid status code (less than 10 or greater than 69). /// - [`LibError::DataNotUtf8`] is returned when metadata (the text after a status code) - /// is not in UTF-8 and cannot be converted to string without errors. + /// is not in UTF-8 and cannot be converted to a string without errors. pub async fn request_with_host( &self, url_str: &str, diff --git a/src/client/tests/mod.rs b/src/client/tests/mod.rs index 49f39d8..3d857b0 100644 --- a/src/client/tests/mod.rs +++ b/src/client/tests/mod.rs @@ -50,9 +50,18 @@ fn check_real_site() { .with_selfsigned_cert_verifier(Verifier {}) .build(); - rt.block_on(client.request("gemini://geminiprotocol.net/docs")) + let resp = rt + .block_on(client.request("gemini://geminiprotocol.net/docs")) .unwrap(); + assert_eq!(resp.is_ok(), true); // check if redirection is processed correctly + + { + let mime = resp.mime().unwrap(); + assert_eq!(mime.type_(), mime::TEXT); + assert_eq!(mime.subtype(), "gemini"); + } + struct Verifier; #[async_trait::async_trait]