diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..dec27ed --- /dev/null +++ b/src/client.rs @@ -0,0 +1,107 @@ +use crate::{error::*, response::Response, status::*}; + +use std::net::ToSocketAddrs; +use std::sync::Arc; + +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; +use tokio_rustls::{ + rustls::{self, pki_types}, + TlsConnector, +}; +use url::Url; + +pub struct Client { + connector: TlsConnector, +} + +impl Default for Client { + fn default() -> Self { + let roots = + rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let config = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + Client::from(config) + } +} + +impl From for Client { + #[inline] + fn from(config: rustls::ClientConfig) -> Self { + Client { + connector: TlsConnector::from(Arc::new(config)), + } + } +} + +impl Client { + pub async fn request(self: &Self, url_str: &str) -> Result { + let url = Url::parse(url_str).map_err(|e| InvalidUrl::ParseError(e))?; + // for proxying http(s) through gemini server, + // use Client::request_with_host + if url.scheme() != "gemini" { + // deny non-Gemini req + return Err(InvalidUrl::SchemeNotGemini.into()); + } + // userinfo (user:pswd@) is not allowed in Gemini + if !url.username().is_empty() { + return Err(InvalidUrl::UserinfoPresent.into()); + } + + let host = url.host_str().ok_or(InvalidUrl::ConvertError)?; + let addr = (host, url.port().unwrap_or(1965)) + .to_socket_addrs()? + .next() + .ok_or(InvalidUrl::ConvertError)?; + + let domain = pki_types::ServerName::try_from(host) + .map_err(|_| InvalidUrl::ConvertError)? + .to_owned(); + + let stream = TcpStream::connect(&addr).await?; + let mut stream = self.connector.connect(domain, stream).await?; + + // Write URL, then CRLF + stream.write_all(url_str.as_bytes()).await?; + stream.write_all(b"\r\n").await?; + + let mut buf: [u8; 3] = [0, 0, 0]; // 2 digits, space + stream.read_exact(&mut buf).await?; + let status = Status::parse_status(&buf)?; + + let mut message: Vec = Vec::new(); + 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 + // until CRLF (\r\n) + loop { + // until CR + buf_reader.read_until(b'\r', &mut message).await?; + // now read next char... + buf_reader.read_exact(&mut buf).await?; + if buf[0] == b'\n' { + // ...and check if it's LF + break; + } 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 + message.push(buf[0].into()); + } + } + + // trim last CR + if message.last().is_some_and(|c| c == &b'\r') { + message.pop(); + } + + // Vec -> ASCII or UTF-8 String + let message = String::from_utf8(message)?; + + Ok(Response::new(status, message, stream)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2b21702..9af7c17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,113 +1,9 @@ +pub mod client; pub mod error; pub mod response; pub mod status; +pub use client::Client; pub use error::*; pub use response::Response; pub use status::*; - -use std::net::ToSocketAddrs; -use std::sync::Arc; - -use tokio::{ - io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}, - net::TcpStream, -}; -use tokio_rustls::{ - rustls::{self, pki_types}, - TlsConnector, -}; -use url::Url; - -pub struct Client { - connector: TlsConnector, -} - -impl Default for Client { - fn default() -> Self { - let roots = - rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = rustls::ClientConfig::builder() - .with_root_certificates(roots) - .with_no_client_auth(); - Client::from(config) - } -} - -impl From for Client { - #[inline] - fn from(config: rustls::ClientConfig) -> Self { - Client { - connector: TlsConnector::from(Arc::new(config)), - } - } -} - -impl Client { - pub async fn request(self: &Self, url_str: &str) -> Result { - let url = Url::parse(url_str).map_err(|e| InvalidUrl::ParseError(e))?; - // for proxying http(s) through gemini server, - // use Client::request_with_host - if url.scheme() != "gemini" { - // deny non-Gemini req - return Err(InvalidUrl::SchemeNotGemini.into()); - } - // userinfo (user:pswd@) is not allowed in Gemini - if !url.username().is_empty() { - return Err(InvalidUrl::UserinfoPresent.into()); - } - - let host = url.host_str().ok_or(InvalidUrl::ConvertError)?; - let addr = (host, url.port().unwrap_or(1965)) - .to_socket_addrs()? - .next() - .ok_or(InvalidUrl::ConvertError)?; - - let domain = pki_types::ServerName::try_from(host) - .map_err(|_| InvalidUrl::ConvertError)? - .to_owned(); - - let stream = TcpStream::connect(&addr).await?; - let mut stream = self.connector.connect(domain, stream).await?; - - // Write URL, then CRLF - stream.write_all(url_str.as_bytes()).await?; - stream.write_all(b"\r\n").await?; - - let mut buf: [u8; 3] = [0, 0, 0]; // 2 digits, space - stream.read_exact(&mut buf).await?; - let status = Status::parse_status(&buf)?; - - let mut message: Vec = Vec::new(); - 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 - // until CRLF (\r\n) - loop { - // until CR - buf_reader.read_until(b'\r', &mut message).await?; - // now read next char... - buf_reader.read_exact(&mut buf).await?; - if buf[0] == b'\n' { - // ...and check if it's LF - break; - } 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 - message.push(buf[0].into()); - } - } - - // trim last CR - if message.last().is_some_and(|c| c == &b'\r') { - message.pop(); - } - - // Vec -> ASCII or UTF-8 String - let message = String::from_utf8(message)?; - - Ok(Response::new(status, message, stream)) - } -}