Compare commits
4 commits
35a4672340
...
fd6702d029
Author | SHA1 | Date | |
---|---|---|---|
fd6702d029 | |||
44d7461ea2 | |||
a77c3d89c8 | |||
8cbeefb713 |
3 changed files with 74 additions and 15 deletions
|
@ -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();
|
||||||
|
|
|
@ -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
42
src/client/tests/mod.rs
Normal 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");
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue