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 response;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub use response::Response;
|
||||
|
||||
#[cfg(feature = "hickory")]
|
||||
|
@ -22,12 +25,14 @@ use builder::ClientBuilder;
|
|||
use std::sync::Arc;
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt},
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_rustls::{rustls, TlsConnector};
|
||||
use tokio_rustls::{client::TlsStream, rustls, TlsConnector};
|
||||
use url::Url;
|
||||
|
||||
pub type ThisResponse = Response<BufReader<TlsStream<TcpStream>>>;
|
||||
|
||||
pub struct Client {
|
||||
pub(crate) connector: TlsConnector,
|
||||
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@` --
|
||||
/// which is forbidden by the Gemini specs.
|
||||
/// - 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)?;
|
||||
// deny non-Gemini requests
|
||||
if url.scheme() != "gemini" {
|
||||
|
@ -83,7 +88,7 @@ impl Client {
|
|||
/// - [`std::io::Error`] is returned in nearly all 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
|
||||
/// 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).
|
||||
/// - [`LibError::DataNotUtf8`] is returned when metadata (the text after a status code)
|
||||
|
@ -93,14 +98,17 @@ impl Client {
|
|||
url_str: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
) -> Result<Response, LibError> {
|
||||
) -> Result<ThisResponse, LibError> {
|
||||
let domain = ServerName::try_from(host)
|
||||
.map_err(|_| InvalidUrl::ConvertError)?
|
||||
.to_owned();
|
||||
|
||||
// TCP connection
|
||||
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 {
|
||||
let cert = stream
|
||||
.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
|
||||
stream.write_all(url_str.as_bytes()).await?;
|
||||
stream.write_all(b"\r\n").await?;
|
||||
|
@ -126,7 +142,7 @@ impl Client {
|
|||
Status::parse_status(&buf)?
|
||||
};
|
||||
|
||||
let mut stream = tokio::io::BufReader::new(stream);
|
||||
let mut stream = BufReader::new(stream);
|
||||
|
||||
let message = {
|
||||
let mut result: Vec<u8> = Vec::new();
|
||||
|
|
|
@ -5,19 +5,17 @@ use crate::{status::Status, LibError, ReplyType};
|
|||
use bytes::Bytes;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
type BodyStream = tokio::io::BufReader<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
/// Client-side response structure wrapping a [`Status`],
|
||||
/// a metadata string and a TLS stream
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub struct Response<IO: AsyncReadExt + Unpin> {
|
||||
status: Status,
|
||||
message: String,
|
||||
stream: BodyStream,
|
||||
stream: IO,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(status: Status, message: String, stream: BodyStream) -> Self {
|
||||
impl<IO: AsyncReadExt + Unpin> Response<IO> {
|
||||
pub fn new(status: Status, message: String, stream: IO) -> Self {
|
||||
Response {
|
||||
status,
|
||||
message,
|
||||
|
@ -35,14 +33,17 @@ impl Response {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # async fn req(client: tokio_gemini::Client) -> Result<(), tokio_gemini::LibError> {
|
||||
/// match client.request("gemini://dc09.ru").await?.ensure_ok() {
|
||||
/// Ok(resp) => {
|
||||
/// Ok(mut resp) => {
|
||||
/// println!("{}", resp.text().await?);
|
||||
/// }
|
||||
/// Err(resp) => {
|
||||
/// println!("{}", resp.message());
|
||||
/// }
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn ensure_ok(self) -> Result<Self, Self> {
|
||||
if self.status.reply_type() == ReplyType::Success {
|
||||
|
@ -82,7 +83,7 @@ impl Response {
|
|||
/// so calling `.bytes()` after `.stream().read_to_end(…)`
|
||||
/// (or `.bytes()` twice, or `.text()` after `.bytes()` and vice versa)
|
||||
/// 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
|
||||
}
|
||||
|
||||
|
|
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