Compare commits

...

4 commits

3 changed files with 74 additions and 15 deletions

View file

@ -3,6 +3,9 @@
pub mod builder; pub mod builder;
pub mod response; pub mod response;
#[cfg(test)]
pub mod tests;
pub use response::Response; pub use response::Response;
#[cfg(feature = "hickory")] #[cfg(feature = "hickory")]
@ -22,12 +25,14 @@ use builder::ClientBuilder;
use std::sync::Arc; use std::sync::Arc;
use tokio::{ use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
net::TcpStream, net::TcpStream,
}; };
use tokio_rustls::{rustls, TlsConnector}; use tokio_rustls::{client::TlsStream, rustls, TlsConnector};
use url::Url; use url::Url;
pub type ThisResponse = Response<BufReader<TlsStream<TcpStream>>>;
pub struct Client { pub struct Client {
pub(crate) connector: TlsConnector, pub(crate) connector: TlsConnector,
pub(crate) ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>, pub(crate) ss_verifier: Option<Arc<dyn SelfsignedCertVerifier>>,
@ -56,7 +61,7 @@ impl Client {
/// - [`InvalidUrl::UserinfoPresent`] is returned when the URL contains userinfo -- `user:password@` -- /// - [`InvalidUrl::UserinfoPresent`] is returned when the URL contains userinfo -- `user:password@` --
/// which is forbidden by the Gemini specs. /// which is forbidden by the Gemini specs.
/// - For the rest, see [`Client::request_with_host`]. /// - For the rest, see [`Client::request_with_host`].
pub async fn request(&self, url_str: &str) -> Result<Response, LibError> { pub async fn request(&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" {
@ -83,7 +88,7 @@ impl Client {
/// - [`std::io::Error`] is returned in nearly all cases: /// - [`std::io::Error`] is returned in nearly all 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 inner error if you need to determine what exactly happened.
/// - [`LibError::StatusOutOfRange`] means that a server returned an incorrect status code /// - [`LibError::StatusOutOfRange`] means that a server returned an incorrect status code
/// (less than 10 or greater than 69). /// (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)
@ -93,14 +98,17 @@ impl Client {
url_str: &str, url_str: &str,
host: &str, host: &str,
port: u16, port: u16,
) -> Result<Response, LibError> { ) -> Result<ThisResponse, LibError> {
let domain = ServerName::try_from(host) let domain = ServerName::try_from(host)
.map_err(|_| InvalidUrl::ConvertError)? .map_err(|_| InvalidUrl::ConvertError)?
.to_owned(); .to_owned();
// TCP connection
let stream = self.try_connect(host, port).await?; let stream = self.try_connect(host, port).await?;
let mut stream = self.connector.connect(domain, stream).await?; // TLS connection via tokio-rustls
let stream = self.connector.connect(domain, stream).await?;
// certificate verification
if let Some(ssv) = &self.ss_verifier { if let Some(ssv) = &self.ss_verifier {
let cert = stream let cert = stream
.get_ref() .get_ref()
@ -115,6 +123,14 @@ impl Client {
} }
} }
self.perform_io(url_str, stream).await
}
pub(crate) async fn perform_io<IO: AsyncReadExt + AsyncWriteExt + Unpin>(
&self,
url_str: &str,
mut stream: IO,
) -> Result<Response<BufReader<IO>>, LibError> {
// Write URL, then CRLF // Write URL, then CRLF
stream.write_all(url_str.as_bytes()).await?; stream.write_all(url_str.as_bytes()).await?;
stream.write_all(b"\r\n").await?; stream.write_all(b"\r\n").await?;
@ -126,7 +142,7 @@ impl Client {
Status::parse_status(&buf)? Status::parse_status(&buf)?
}; };
let mut stream = tokio::io::BufReader::new(stream); let mut stream = BufReader::new(stream);
let message = { let message = {
let mut result: Vec<u8> = Vec::new(); let mut result: Vec<u8> = Vec::new();

View file

@ -5,19 +5,17 @@ use crate::{status::Status, LibError, ReplyType};
use bytes::Bytes; use bytes::Bytes;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
type BodyStream = tokio::io::BufReader<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
/// Client-side response structure wrapping a [`Status`], /// Client-side response structure wrapping a [`Status`],
/// a metadata string and a TLS stream /// a metadata string and a TLS stream
#[derive(Debug)] #[derive(Debug)]
pub struct Response { pub struct Response<IO: AsyncReadExt + Unpin> {
status: Status, status: Status,
message: String, message: String,
stream: BodyStream, stream: IO,
} }
impl Response { impl<IO: AsyncReadExt + Unpin> Response<IO> {
pub fn new(status: Status, message: String, stream: BodyStream) -> Self { pub fn new(status: Status, message: String, stream: IO) -> Self {
Response { Response {
status, status,
message, message,
@ -35,14 +33,17 @@ impl Response {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # async fn req(client: tokio_gemini::Client) -> Result<(), tokio_gemini::LibError> {
/// match client.request("gemini://dc09.ru").await?.ensure_ok() { /// match client.request("gemini://dc09.ru").await?.ensure_ok() {
/// Ok(resp) => { /// Ok(mut resp) => {
/// println!("{}", resp.text().await?); /// println!("{}", resp.text().await?);
/// } /// }
/// Err(resp) => { /// Err(resp) => {
/// println!("{}", resp.message()); /// println!("{}", resp.message());
/// } /// }
/// } /// }
/// # Ok(())
/// # }
/// ``` /// ```
pub fn ensure_ok(self) -> Result<Self, Self> { pub fn ensure_ok(self) -> Result<Self, Self> {
if self.status.reply_type() == ReplyType::Success { if self.status.reply_type() == ReplyType::Success {
@ -82,7 +83,7 @@ impl Response {
/// so calling `.bytes()` after `.stream().read_to_end(…)` /// so calling `.bytes()` after `.stream().read_to_end(…)`
/// (or `.bytes()` twice, or `.text()` after `.bytes()` and vice versa) /// (or `.bytes()` twice, or `.text()` after `.bytes()` and vice versa)
/// on the same response will result in empty output. /// on the same response will result in empty output.
pub fn stream(&mut self) -> &mut BodyStream { pub fn stream(&mut self) -> &mut IO {
&mut self.stream &mut self.stream
} }

42
src/client/tests/mod.rs Normal file
View file

@ -0,0 +1,42 @@
use tokio::runtime::Runtime;
use super::*;
#[test]
fn check_parser() {
let rt = Runtime::new().unwrap();
let client = Client::builder().dangerous_with_no_verifier().build();
let mut recv = Vec::new();
let stream = tokio::io::join(
"20 text/gemini\r\n# hello world\n👍\n".as_bytes(),
&mut recv,
);
let mut resp = rt
.block_on(client.perform_io("gemini://unw.dc09.ru", stream))
.unwrap();
{
let status = resp.status();
assert_eq!(status.status_code(), StatusCode::Success);
assert_eq!(status.reply_type(), ReplyType::Success);
assert_eq!(status.num(), 20u8);
assert_eq!(status.second_digit(), 0u8);
}
assert_eq!(resp.is_ok(), true);
assert_eq!(resp.message(), "text/gemini");
{
let mime = resp.mime().unwrap();
assert_eq!(mime.type_(), mime::TEXT);
assert_eq!(mime.subtype(), "gemini");
}
assert_eq!(rt.block_on(resp.text()).unwrap(), "# hello world\n👍\n");
drop(resp); // to free recv from mutable borrowing
assert_eq!(recv.as_slice(), b"gemini://unw.dc09.ru\r\n");
}