Compare commits
2 commits
fd6702d029
...
6a8d628546
Author | SHA1 | Date | |
---|---|---|---|
6a8d628546 | |||
d1fc9f278b |
2 changed files with 101 additions and 18 deletions
|
@ -49,19 +49,55 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Parse the given URL, do some checks,
|
/// Perform a Gemini request with the specified URL.
|
||||||
/// take host and port (default is 1965) from the URL
|
/// Host and port (1965 by default) are parsed from `url_str`
|
||||||
/// and perform a Gemini request, returning [`Response`] on success.
|
/// 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
|
/// # Errors
|
||||||
/// Everything is converted into [`LibError`].
|
/// - See [`Client::request_with_no_redirect`].
|
||||||
/// - [`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`].
|
|
||||||
pub async fn request(&self, url_str: &str) -> Result<ThisResponse, LibError> {
|
pub async fn request(&self, url_str: &str) -> Result<ThisResponse, LibError> {
|
||||||
|
// 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<ThisResponse, LibError> {
|
||||||
let url = Url::parse(url_str).map_err(InvalidUrl::ParseError)?;
|
let url = Url::parse(url_str).map_err(InvalidUrl::ParseError)?;
|
||||||
// deny non-Gemini requests
|
// deny non-Gemini requests
|
||||||
if url.scheme() != "gemini" {
|
if url.scheme() != "gemini" {
|
||||||
|
@ -82,17 +118,22 @@ impl Client {
|
||||||
/// Non-`gemini://` URLs is OK if the remote server supports proxying.
|
/// Non-`gemini://` URLs is OK if the remote server supports proxying.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Everything is converted into [`LibError`].
|
/// - [`InvalidUrl::ConvertError`] means that a hostname cannot be
|
||||||
/// - [`InvalidUrl::ConvertError`] is an error while converting
|
/// converted into [`pki_types::ServerName`].
|
||||||
/// host and port into [`std::net::SocketAddr`] or [`pki_types::ServerName`].
|
/// - [`LibError::HostLookupError`] means that a DNS server returned no records,
|
||||||
/// - [`std::io::Error`] is returned in nearly all cases:
|
/// 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,
|
/// could not open a TCP connection, perform a TLS handshake,
|
||||||
/// write to or read from the TCP stream.
|
/// write to or read from the TCP stream.
|
||||||
/// Check the inner error if you need to determine what exactly happened.
|
/// Check the ErrorKind and/or the inner error
|
||||||
/// - [`LibError::StatusOutOfRange`] means that a server returned an incorrect status code
|
/// if you need to determine what exactly happened.
|
||||||
/// (less than 10 or greater than 69).
|
/// - [`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)
|
/// - [`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(
|
pub async fn request_with_host(
|
||||||
&self,
|
&self,
|
||||||
url_str: &str,
|
url_str: &str,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
use crate::certs::{fingerprint::CertFingerprint, CertificateDer};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -40,3 +42,43 @@ fn check_parser() {
|
||||||
drop(resp); // to free recv from mutable borrowing
|
drop(resp); // to free recv from mutable borrowing
|
||||||
assert_eq!(recv.as_slice(), b"gemini://unw.dc09.ru\r\n");
|
assert_eq!(recv.as_slice(), b"gemini://unw.dc09.ru\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_real_site() {
|
||||||
|
let rt = Runtime::new().unwrap();
|
||||||
|
let client = Client::builder()
|
||||||
|
.with_selfsigned_cert_verifier(Verifier {})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
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]
|
||||||
|
impl SelfsignedCertVerifier for Verifier {
|
||||||
|
async fn verify(
|
||||||
|
&self,
|
||||||
|
cert: &CertificateDer<'_>,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
) -> Result<bool, LibError> {
|
||||||
|
assert_eq!(host, "geminiprotocol.net");
|
||||||
|
assert_eq!(port, 1965);
|
||||||
|
assert_eq!(
|
||||||
|
CertFingerprint::new_sha256(cert).base64(),
|
||||||
|
"OBuOKRLSTQcgHXdQ0QFshcGQSgc5o+g0fnHDY+7SolE",
|
||||||
|
);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue