Compare commits

..

No commits in common. "86fb310e71a6888896d898df4bf0ab0a7ca44f73" and "5dc0dadaa9b6f38d2b4369c49d1441c0813052a9" have entirely different histories.

2 changed files with 106 additions and 120 deletions

View file

@ -1,118 +0,0 @@
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<rustls::ClientConfig> 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<Response, LibError> {
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 port = url.port().unwrap_or(1965);
self.request_with_host(url_str, host, port).await
}
pub async fn request_with_host(
self: &Self,
url_str: &str,
host: &str,
port: u16,
) -> Result<Response, LibError> {
let addr = (host, port)
.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<u8> = 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<u8> -> ASCII or UTF-8 String
let message = String::from_utf8(message)?;
Ok(Response::new(status, message, stream))
}
}

View file

@ -1,9 +1,113 @@
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<rustls::ClientConfig> 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<Response, LibError> {
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<u8> = 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<u8> -> ASCII or UTF-8 String
let message = String::from_utf8(message)?;
Ok(Response::new(status, message, stream))
}
}