Compare commits

...

5 commits

Author SHA1 Message Date
1395bb04ff
fix: CryptoProvider::get_default returned None 2024-08-06 18:36:40 +04:00
39c31d9651
important fix for buffered stream + code cleanup
a `TlsStream<TcpStream>` was being wrapped in a `BufReader` during header parsing,
but then the pure stream (without BufReader) was being passed into a Response constructor,
discarding all content in an internal BufReader's buffer,
resulting in an incomplete body (truncated at the beginning)
or empty at all
2024-08-06 18:33:11 +04:00
4f52475f2c
add simple.rs example 2024-08-06 12:32:42 +04:00
dbbcf322c3
feat: add Status::num() for handy conversion enum->u8 2024-08-06 12:27:48 +04:00
6bd101504d
feat: add ensure_ok for simple 20/non-20 matching 2024-08-06 12:26:59 +04:00
6 changed files with 110 additions and 31 deletions

View file

@ -20,6 +20,10 @@ tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring
url = "2.5.2" url = "2.5.2"
webpki-roots = "0.26.3" webpki-roots = "0.26.3"
[[example]]
name = "simple"
path = "examples/simple.rs"
[[example]] [[example]]
name = "main" name = "main"
path = "examples/main.rs" path = "examples/main.rs"

54
examples/simple.rs Normal file
View file

@ -0,0 +1,54 @@
use tokio_gemini::{
certs::{
fingerprint::{generate_fingerprint, Algorithm},
verifier::SelfsignedCertVerifier,
},
Client, LibError,
};
// Much simpler than examples/main.rs
// Hardcoded URL, no cert check, always write to stdout
//
// cargo add tokio-gemini
// cargo add tokio -F macros,rt-multi-thread
//
const URL: &str = "gemini://geminiprotocol.net/docs/protocol-specification.gmi";
#[tokio::main]
async fn main() -> Result<(), LibError> {
let client = Client::builder()
.with_selfsigned_cert_verifier(CertVerifier)
.build();
match client.request(URL).await?.ensure_ok() {
Ok(mut resp) => {
println!("{}", resp.text().await?);
}
Err(resp) => {
println!("{} {}", resp.status().num(), resp.message());
}
}
Ok(())
}
struct CertVerifier;
impl SelfsignedCertVerifier for CertVerifier {
fn verify(
&self,
cert: &tokio_gemini::certs::verifier::CertificateDer,
host: &str,
_now: tokio_gemini::certs::verifier::UnixTime,
) -> Result<bool, tokio_rustls::rustls::Error> {
// For real verification example with known_hosts file
// see examples/main.rs
eprintln!(
"Host = {}\nFingerprint = {}",
host,
generate_fingerprint(cert, Algorithm::Sha512),
);
Ok(true)
}
}

View file

@ -9,7 +9,11 @@ pub struct AllowAllCertVerifier(std::sync::Arc<CryptoProvider>);
impl AllowAllCertVerifier { impl AllowAllCertVerifier {
pub fn yes_i_know_what_i_am_doing() -> Self { pub fn yes_i_know_what_i_am_doing() -> Self {
AllowAllCertVerifier(CryptoProvider::get_default().unwrap().clone()) AllowAllCertVerifier(
CryptoProvider::get_default()
.map(|c| c.clone())
.unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::ring::default_provider())),
)
} }
} }

View file

@ -85,39 +85,44 @@ impl Client {
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?;
let mut buf: [u8; 3] = [0, 0, 0]; // 2 digits, space let status = {
stream.read_exact(&mut buf).await?; let mut buf: [u8; 3] = [0, 0, 0]; // 2 digits, space
let status = Status::parse_status(&buf)?; stream.read_exact(&mut buf).await?;
Status::parse_status(&buf)?
};
let mut message: Vec<u8> = Vec::new(); let mut stream = tokio::io::BufReader::new(stream);
let mut buf_reader = tokio::io::BufReader::new(&mut stream);
let mut buf: [u8; 1] = [0]; // buffer for LF (\n)
// reading message after status code let message = {
// until CRLF (\r\n) let mut result: Vec<u8> = Vec::new();
loop { let mut buf = [0u8]; // buffer for LF (\n)
// until CR
buf_reader.read_until(b'\r', &mut message).await?; // reading message after status code
// now read next char... // until CRLF (\r\n)
buf_reader.read_exact(&mut buf).await?; loop {
if buf[0] == b'\n' { // until CR
// ...and check if it's LF stream.read_until(b'\r', &mut result).await?;
break; // now read next char...
} else { stream.read_exact(&mut buf).await?;
// ...otherwise, CR is a part of message, not a CRLF terminator, if buf[0] == b'\n' {
// so append that one byte that's supposed to be LF (but not LF) // ...and check if it's LF
// to the message buffer break;
message.push(buf[0].into()); } else {
// ...otherwise, CR is a part of message, not a CRLF terminator,
// so append that one byte that's supposed to be LF (but not LF)
// to the message buffer
result.push(buf[0].into());
}
} }
}
// trim last CR // trim last CR
if message.last().is_some_and(|c| c == &b'\r') { if result.last().is_some_and(|c| c == &b'\r') {
message.pop(); result.pop();
} }
// Vec<u8> -> ASCII or UTF-8 String // Vec<u8> -> ASCII or UTF-8 String
let message = String::from_utf8(message)?; String::from_utf8(result)?
};
Ok(Response::new(status, message, stream)) Ok(Response::new(status, message, stream))
} }

View file

@ -1,9 +1,9 @@
use crate::{status::Status, LibError}; use crate::{status::Status, LibError, ReplyType};
use bytes::Bytes; use bytes::Bytes;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
type BodyStream = tokio_rustls::client::TlsStream<tokio::net::TcpStream>; type BodyStream = tokio::io::BufReader<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
#[derive(Debug)] #[derive(Debug)]
pub struct Response { pub struct Response {
@ -21,6 +21,14 @@ impl Response {
} }
} }
pub fn ensure_ok(self: Self) -> Result<Self, Self> {
if self.status.reply_type() == ReplyType::Success {
Ok(self)
} else {
Err(self)
}
}
pub fn status(self: &Self) -> Status { pub fn status(self: &Self) -> Status {
self.status self.status
} }

View file

@ -81,6 +81,10 @@ impl Status {
self.status_code self.status_code
} }
pub fn num(self: &Self) -> u8 {
self.status_code.into()
}
pub fn reply_type(self: &Self) -> ReplyType { pub fn reply_type(self: &Self) -> ReplyType {
self.reply_type self.reply_type
} }