feat: basic client (untested)
This commit is contained in:
parent
aceebec883
commit
4cf9278cad
6 changed files with 443 additions and 2 deletions
49
src/error.rs
Normal file
49
src/error.rs
Normal 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,
|
||||
}
|
60
src/lib.rs
60
src/lib.rs
|
@ -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
35
src/response.rs
Normal 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
77
src/status.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue