Compare commits
5 commits
053f042e00
...
1395bb04ff
Author | SHA1 | Date | |
---|---|---|---|
1395bb04ff | |||
39c31d9651 | |||
4f52475f2c | |||
dbbcf322c3 | |||
6bd101504d |
6 changed files with 110 additions and 31 deletions
|
@ -20,6 +20,10 @@ tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring
|
|||
url = "2.5.2"
|
||||
webpki-roots = "0.26.3"
|
||||
|
||||
[[example]]
|
||||
name = "simple"
|
||||
path = "examples/simple.rs"
|
||||
|
||||
[[example]]
|
||||
name = "main"
|
||||
path = "examples/main.rs"
|
||||
|
|
54
examples/simple.rs
Normal file
54
examples/simple.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,11 @@ pub struct AllowAllCertVerifier(std::sync::Arc<CryptoProvider>);
|
|||
|
||||
impl AllowAllCertVerifier {
|
||||
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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,21 +85,25 @@ impl Client {
|
|||
stream.write_all(url_str.as_bytes()).await?;
|
||||
stream.write_all(b"\r\n").await?;
|
||||
|
||||
let status = {
|
||||
let mut buf: [u8; 3] = [0, 0, 0]; // 2 digits, space
|
||||
stream.read_exact(&mut buf).await?;
|
||||
let status = Status::parse_status(&buf)?;
|
||||
Status::parse_status(&buf)?
|
||||
};
|
||||
|
||||
let mut message: Vec<u8> = Vec::new();
|
||||
let mut buf_reader = tokio::io::BufReader::new(&mut stream);
|
||||
let mut buf: [u8; 1] = [0]; // buffer for LF (\n)
|
||||
let mut stream = tokio::io::BufReader::new(stream);
|
||||
|
||||
let message = {
|
||||
let mut result: Vec<u8> = Vec::new();
|
||||
let mut buf = [0u8]; // buffer for LF (\n)
|
||||
|
||||
// reading message after status code
|
||||
// until CRLF (\r\n)
|
||||
loop {
|
||||
// until CR
|
||||
buf_reader.read_until(b'\r', &mut message).await?;
|
||||
stream.read_until(b'\r', &mut result).await?;
|
||||
// now read next char...
|
||||
buf_reader.read_exact(&mut buf).await?;
|
||||
stream.read_exact(&mut buf).await?;
|
||||
if buf[0] == b'\n' {
|
||||
// ...and check if it's LF
|
||||
break;
|
||||
|
@ -107,17 +111,18 @@ impl Client {
|
|||
// ...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
|
||||
message.push(buf[0].into());
|
||||
result.push(buf[0].into());
|
||||
}
|
||||
}
|
||||
|
||||
// trim last CR
|
||||
if message.last().is_some_and(|c| c == &b'\r') {
|
||||
message.pop();
|
||||
if result.last().is_some_and(|c| c == &b'\r') {
|
||||
result.pop();
|
||||
}
|
||||
|
||||
// Vec<u8> -> ASCII or UTF-8 String
|
||||
let message = String::from_utf8(message)?;
|
||||
String::from_utf8(result)?
|
||||
};
|
||||
|
||||
Ok(Response::new(status, message, stream))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use crate::{status::Status, LibError};
|
||||
use crate::{status::Status, LibError, ReplyType};
|
||||
|
||||
use bytes::Bytes;
|
||||
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)]
|
||||
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 {
|
||||
self.status
|
||||
}
|
||||
|
|
|
@ -81,6 +81,10 @@ impl Status {
|
|||
self.status_code
|
||||
}
|
||||
|
||||
pub fn num(self: &Self) -> u8 {
|
||||
self.status_code.into()
|
||||
}
|
||||
|
||||
pub fn reply_type(self: &Self) -> ReplyType {
|
||||
self.reply_type
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue