feat: basic client (untested)

This commit is contained in:
DarkCat09 2024-07-31 22:14:29 +04:00
parent aceebec883
commit 4cf9278cad
Signed by: DarkCat09
GPG key ID: 0A26CD5B3345D6E3
6 changed files with 443 additions and 2 deletions

49
src/error.rs Normal file
View file

@ -0,0 +1,49 @@
pub enum LibError {
IoError(std::io::Error),
InvalidUrlError(InvalidUrl),
StatusOutOfRange(u8),
MessageNotUtf8(std::string::FromUtf8Error),
}
impl From<std::io::Error> for LibError {
#[inline]
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<url::ParseError> for LibError {
#[inline]
fn from(err: url::ParseError) -> Self {
Self::InvalidUrlError(InvalidUrl::ParseError(err))
}
}
impl From<InvalidUrl> for LibError {
#[inline]
fn from(err: InvalidUrl) -> Self {
Self::InvalidUrlError(err)
}
}
impl LibError {
#[inline]
pub fn status_out_of_range(num: u8) -> Self {
Self::StatusOutOfRange(num)
}
}
impl From<std::string::FromUtf8Error> for LibError {
#[inline]
fn from(err: std::string::FromUtf8Error) -> Self {
Self::MessageNotUtf8(err)
}
}
pub enum InvalidUrl {
ParseError(url::ParseError),
SchemeNotGemini,
UserinfoPresent,
NoHostFound,
ConvertError,
}

View file

@ -1,6 +1,23 @@
mod error;
mod response;
mod status;
use error::*;
use response::Response;
use status::Status;
use std::net::ToSocketAddrs;
use std::sync::Arc;
use tokio_rustls::{rustls, TlsConnector};
use tokio::{
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
use tokio_rustls::{
rustls::{self, pki_types},
TlsConnector,
};
use url::Url;
pub struct Client {
connector: TlsConnector,
@ -25,3 +42,44 @@ impl From<rustls::ClientConfig> for Client {
}
}
}
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))?;
if url.scheme() != "gemini" {
return Err(InvalidUrl::SchemeNotGemini.into());
}
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?;
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];
loop {
buf_reader.read_until(b'\r', &mut message).await?;
buf_reader.read_exact(&mut buf).await?;
if buf[0] == b'\n' {
break;
} else {
message.push(buf[0].into());
}
}
let message = String::from_utf8(message)?;
Ok(Response::new(status, message, stream))
}
}

35
src/response.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::status::Status;
type BodyStream = tokio_rustls::client::TlsStream<tokio::net::TcpStream>;
pub struct Response {
status: Status,
message: String,
body: BodyStream,
}
impl Response {
pub fn new(status: Status, message: String, body: BodyStream) -> Self {
Response {
status,
message,
body,
}
}
pub fn status(self: &Self) -> Status {
self.status
}
pub fn message(self: &Self) -> &str {
&self.message
}
pub fn mime(self: &Self) -> Result<mime::Mime, mime::FromStrError> {
self.message.parse()
}
pub fn body(self: &Self) -> &BodyStream {
&self.body
}
}

77
src/status.rs Normal file
View file

@ -0,0 +1,77 @@
use crate::error::LibError;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[derive(Clone, Copy)]
pub struct Status {
status_code: StatusCode,
reply_type: ReplyType,
second_digit: u8,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[num_enum(error_type(name = LibError, constructor = LibError::status_out_of_range))]
#[repr(u8)]
pub enum ReplyType {
Input = 1,
Success,
Redirect,
TempFail,
PermFail,
Auth,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[num_enum(error_type(name = LibError, constructor = LibError::status_out_of_range))]
#[repr(u8)]
pub enum StatusCode {
Input = 10,
InputSensitive = 11,
Success = 20,
TempRedirect = 30,
PermRedirect = 31,
TempFail = 40,
ServerUnavailable = 41,
CgiError = 42,
ProxyError = 43,
SlowDown = 44,
PermFail = 50,
NotFound = 51,
Gone = 52,
ProxyRequestRefused = 53,
BadRequest = 59,
ClientCerts = 60,
CertNotAuthorized = 61,
CertNotValid = 62,
}
const ASCII_ZERO: u8 = 48; // '0'
impl Status {
pub fn parse_status(buf: &[u8]) -> Result<Self, LibError> {
let first = buf[0] - ASCII_ZERO;
let second = buf[1] - ASCII_ZERO;
Ok(Status {
status_code: StatusCode::try_from_primitive(first * 10 + second)?,
reply_type: ReplyType::try_from_primitive(first)?,
second_digit: second,
})
}
pub fn status_code(self: &Self) -> StatusCode {
self.status_code
}
pub fn reply_type(self: &Self) -> ReplyType {
self.reply_type
}
pub fn second_digit(self: &Self) -> u8 {
self.second_digit
}
}