From 4cf9278cad8d06d3e7250b2c7b03b18941979d0e Mon Sep 17 00:00:00 2001 From: DarkCat09 Date: Wed, 31 Jul 2024 22:14:29 +0400 Subject: [PATCH] feat: basic client (untested) --- Cargo.lock | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- src/error.rs | 49 +++++++++++ src/lib.rs | 60 ++++++++++++- src/response.rs | 35 ++++++++ src/status.rs | 77 +++++++++++++++++ 6 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 src/error.rs create mode 100644 src/response.rs create mode 100644 src/status.rs diff --git a/Cargo.lock b/Cargo.lock index 7a378ef..17e1bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 854a208..bba3e15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..330dde5 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,49 @@ +pub enum LibError { + IoError(std::io::Error), + InvalidUrlError(InvalidUrl), + StatusOutOfRange(u8), + MessageNotUtf8(std::string::FromUtf8Error), +} + +impl From for LibError { + #[inline] + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} + +impl From for LibError { + #[inline] + fn from(err: url::ParseError) -> Self { + Self::InvalidUrlError(InvalidUrl::ParseError(err)) + } +} + +impl From 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 for LibError { + #[inline] + fn from(err: std::string::FromUtf8Error) -> Self { + Self::MessageNotUtf8(err) + } +} + +pub enum InvalidUrl { + ParseError(url::ParseError), + SchemeNotGemini, + UserinfoPresent, + NoHostFound, + ConvertError, +} diff --git a/src/lib.rs b/src/lib.rs index cf39e2c..598a081 100644 --- a/src/lib.rs +++ b/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 for Client { } } } + +impl Client { + pub async fn request(self: &Self, url_str: &str) -> Result { + 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 = 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)) + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..6b2cc8f --- /dev/null +++ b/src/response.rs @@ -0,0 +1,35 @@ +use crate::status::Status; + +type BodyStream = tokio_rustls::client::TlsStream; + +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 { + self.message.parse() + } + + pub fn body(self: &Self) -> &BodyStream { + &self.body + } +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..3487af9 --- /dev/null +++ b/src/status.rs @@ -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 { + 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 + } +}