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

220
Cargo.lock generated
View file

@ -32,6 +32,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "bytes"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "cc"
version = "1.1.7"
@ -44,6 +50,21 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@ -61,6 +82,38 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "libc"
version = "0.2.155"
@ -88,6 +141,39 @@ dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
dependencies = [
"hermit-abi",
"libc",
"wasi",
"windows-sys",
]
[[package]]
name = "num_enum"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "object"
version = "0.36.2"
@ -103,12 +189,45 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ring"
version = "0.17.8"
@ -161,6 +280,16 @@ dependencies = [
"untrusted",
]
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "spin"
version = "0.9.8"
@ -173,6 +302,32 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.39.2"
@ -180,7 +335,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"pin-project-lite",
"socket2",
"windows-sys",
]
[[package]]
@ -188,8 +348,10 @@ name = "tokio-gemini"
version = "0.1.0"
dependencies = [
"mime",
"num_enum",
"tokio",
"tokio-rustls",
"url",
"webpki-roots",
]
@ -204,12 +366,61 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -298,6 +509,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "zeroize"
version = "1.8.1"

View file

@ -5,6 +5,8 @@ edition = "2021"
[dependencies]
mime = "0.3.17"
tokio = "1.39.2"
num_enum = "0.7.3"
tokio = { version = "1.39.2", features = ["io-util", "net"] }
tokio-rustls = { version = "0.26.0", default-features = false, features = ["ring"] }
url = "2.5.2"
webpki-roots = "0.26.3"

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
}
}