feat: basic client (untested)
This commit is contained in:
parent
aceebec883
commit
4cf9278cad
6 changed files with 443 additions and 2 deletions
220
Cargo.lock
generated
220
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
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…
Reference in a new issue