Compare commits
2 commits
5dc0dadaa9
...
86fb310e71
Author | SHA1 | Date | |
---|---|---|---|
86fb310e71 | |||
d462cd4fbe |
2 changed files with 120 additions and 106 deletions
118
src/client.rs
Normal file
118
src/client.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
108
src/lib.rs
108
src/lib.rs
|
@ -1,113 +1,9 @@
|
||||||
|
pub mod client;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
|
pub use client::Client;
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
pub use status::*;
|
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue