use std::time::Duration;
const MAX_REQUEST_URL_BYTES: usize = 1024;
const DEFAULT_GEMINI_PORT: u16 = 1965;
mod response;
mod status;
mod trust;
#[cfg(feature = "blocking")] pub mod blocking;
#[cfg(feature = "request")] pub mod non_blocking;
#[cfg(feature = "request")] pub use non_blocking::request;
pub use {
response::Response,
status::{Status, StatusCategory},
trust::{CertificateStore, default_root_certificates},
};
#[derive(Clone, Debug)]
pub struct RequestOptions {
pub root_certificates: rustls::RootCertStore,
pub timeout: Duration,
pub max_response_bytes: usize,
}
impl RequestOptions {
const fn with_roots(root_certificates: rustls::RootCertStore) -> Self {
Self {
root_certificates,
timeout: Duration::from_secs(30),
max_response_bytes: 16 * 1024 * 1024,
}
}
}
impl Default for RequestOptions {
fn default() -> Self { Self::with_roots(default_root_certificates()) }
}
pub(crate) fn request_line(url: &url::Url) -> anyhow::Result<String> {
if url.scheme() == "gemini" {
anyhow::ensure!(
url.username().is_empty() && url.password().is_none(),
"Gemini URLs cannot contain user information"
);
}
let mut request_url = url.clone();
request_url.set_fragment(None);
let url_bytes = request_url.as_str().as_bytes();
anyhow::ensure!(
url_bytes.len() <= MAX_REQUEST_URL_BYTES,
"Gemini URL exceeds {MAX_REQUEST_URL_BYTES} bytes"
);
anyhow::ensure!(
!url_bytes.contains(&b'\r') && !url_bytes.contains(&b'\n'),
"Gemini URL contains a line break"
);
Ok(format!("{request_url}\r\n"))
}
#[cfg(test)]
mod tests {
use {
super::{MAX_REQUEST_URL_BYTES, request_line},
url::Url,
};
#[test]
fn request_line_omits_fragment() {
let url = Url::parse("gemini://example.com/path?query#fragment").unwrap();
assert_eq!(
request_line(&url).unwrap(),
"gemini://example.com/path?query\r\n"
);
}
#[test]
fn request_line_enforces_url_byte_limit() {
let prefix = "gemini://example.com/";
let accepted = Url::parse(&format!(
"{prefix}{}",
"a".repeat(MAX_REQUEST_URL_BYTES - prefix.len())
))
.unwrap();
let rejected = Url::parse(&format!(
"{prefix}{}",
"a".repeat(MAX_REQUEST_URL_BYTES + 1 - prefix.len())
))
.unwrap();
assert_eq!(
request_line(&accepted).unwrap().len(),
MAX_REQUEST_URL_BYTES + b"\r\n".len()
);
assert!(request_line(&rejected).is_err());
}
#[test]
fn request_line_counts_encoded_unicode_bytes() {
let prefix = "gemini://example.com/";
let encoded_character_bytes = "%C3%A9".len();
let accepted = Url::parse(&format!(
"{prefix}{}é",
"a"
.repeat(MAX_REQUEST_URL_BYTES - prefix.len() - encoded_character_bytes)
))
.unwrap();
let rejected = Url::parse(&format!(
"{prefix}{}é",
"a".repeat(
MAX_REQUEST_URL_BYTES + 1 - prefix.len() - encoded_character_bytes
)
))
.unwrap();
assert_eq!(
request_line(&accepted).unwrap().len(),
MAX_REQUEST_URL_BYTES + b"\r\n".len()
);
assert!(request_line(&rejected).is_err());
}
#[test]
fn request_line_rejects_user_information() {
let url = Url::parse("gemini://user:secret@example.com/").unwrap();
assert!(request_line(&url).is_err());
}
}