Compare commits
No commits in common. "fd6702d029707786d96fdefe69df37278cb92925" and "35a4672340a505d5481f5949e1bdef16011472fc" have entirely different histories.
fd6702d029
...
35a4672340
3 changed files with 15 additions and 74 deletions
|
@ -3,9 +3,6 @@
|
||||||
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")]
|
||||||
|
@ -25,14 +22,12 @@ use builder::ClientBuilder;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt},
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
};
|
};
|
||||||
use tokio_rustls::{client::TlsStream, rustls, TlsConnector};
|
use tokio_rustls::{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>>,
|
||||||
|
@ -61,7 +56,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<ThisResponse, LibError> {
|
pub async fn request(&self, url_str: &str) -> Result<Response, 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" {
|
||||||
|
@ -88,7 +83,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)
|
||||||
|
@ -98,17 +93,14 @@ impl Client {
|
||||||
url_str: &str,
|
url_str: &str,
|
||||||
host: &str,
|
host: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
) -> Result<ThisResponse, LibError> {
|
) -> Result<Response, 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?;
|
||||||
// TLS connection via tokio-rustls
|
let mut stream = self.connector.connect(domain, stream).await?;
|
||||||
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()
|
||||||
|
@ -123,14 +115,6 @@ 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?;
|
||||||
|
@ -142,7 +126,7 @@ impl Client {
|
||||||
Status::parse_status(&buf)?
|
Status::parse_status(&buf)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut stream = BufReader::new(stream);
|
let mut stream = tokio::io::BufReader::new(stream);
|
||||||
|
|
||||||
let message = {
|
let message = {
|
||||||
let mut result: Vec<u8> = Vec::new();
|
let mut result: Vec<u8> = Vec::new();
|
||||||
|
|
|
@ -5,17 +5,19 @@ 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<IO: AsyncReadExt + Unpin> {
|
pub struct Response {
|
||||||
status: Status,
|
status: Status,
|
||||||
message: String,
|
message: String,
|
||||||
stream: IO,
|
stream: BodyStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<IO: AsyncReadExt + Unpin> Response<IO> {
|
impl Response {
|
||||||
pub fn new(status: Status, message: String, stream: IO) -> Self {
|
pub fn new(status: Status, message: String, stream: BodyStream) -> Self {
|
||||||
Response {
|
Response {
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
|
@ -33,17 +35,14 @@ impl<IO: AsyncReadExt + Unpin> Response<IO> {
|
||||||
///
|
///
|
||||||
/// # 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(mut resp) => {
|
/// Ok(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 {
|
||||||
|
@ -83,7 +82,7 @@ impl<IO: AsyncReadExt + Unpin> Response<IO> {
|
||||||
/// 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 IO {
|
pub fn stream(&mut self) -> &mut BodyStream {
|
||||||
&mut self.stream
|
&mut self.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
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