draft new api version

This commit is contained in:
yggverse 2024-11-27 05:50:09 +02:00
parent 67d486cc4d
commit 3a9e84a3d9
19 changed files with 490 additions and 87 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "ggemini"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
license = "MIT"
readme = "README.md"

View file

@ -1,4 +1,128 @@
//! Client API to interact Server using
//! [Gemini protocol](https://geminiprotocol.net/docs/protocol-specification.gmi)
//! High-level client API to interact with Gemini Socket Server:
//! * https://geminiprotocol.net/docs/protocol-specification.gmi
pub mod connection;
pub mod error;
pub mod response;
pub use connection::Connection;
pub use error::Error;
pub use response::Response;
use gio::{
prelude::{IOStreamExt, OutputStreamExt, SocketClientExt},
Cancellable, NetworkAddress, SocketClient, SocketProtocol, TlsCertificate,
};
use glib::{Bytes, Priority, Uri};
pub const DEFAULT_PORT: u16 = 1965;
pub const DEFAULT_TIMEOUT: u32 = 10;
pub struct Client {
pub socket: SocketClient,
}
impl Client {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
let socket = SocketClient::new();
socket.set_protocol(SocketProtocol::Tcp);
socket.set_timeout(DEFAULT_TIMEOUT);
Self { socket }
}
// Actions
/// Make async request to given [Uri](https://docs.gtk.org/glib/struct.Uri.html),
/// callback with `Result`on success or `Error` on failure.
/// * creates new [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
/// * session management by Glib TLS Backend
pub fn request_async(
&self,
uri: Uri,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
certificate: Option<TlsCertificate>,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
match network_address_for(&uri) {
Ok(network_address) => {
self.socket.connect_async(
&network_address.clone(),
match cancellable {
Some(ref cancellable) => Some(cancellable.clone()),
None => None::<Cancellable>,
}
.as_ref(),
move |result| match result {
Ok(connection) => {
match Connection::from(network_address, connection, certificate) {
Ok(result) => request_async(
result,
uri.to_string(),
match priority {
Some(priority) => priority,
None => Priority::DEFAULT,
},
cancellable.unwrap(), // @TODO
move |result| callback(result),
),
Err(reason) => callback(Err(Error::Connection(reason))),
}
}
Err(reason) => callback(Err(Error::Connect(reason))),
},
);
}
Err(reason) => callback(Err(reason)),
};
}
}
// Private helpers
/// [SocketConnectable](https://docs.gtk.org/gio/iface.SocketConnectable.html) /
/// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
fn network_address_for(uri: &Uri) -> Result<NetworkAddress, Error> {
Ok(NetworkAddress::new(
&match uri.host() {
Some(host) => host,
None => return Err(Error::Connectable(uri.to_string())),
},
if uri.port().is_positive() {
uri.port() as u16
} else {
DEFAULT_PORT
},
))
}
fn request_async(
connection: Connection,
query: String,
priority: Priority,
cancellable: Cancellable,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
connection.stream().output_stream().write_bytes_async(
&Bytes::from(format!("{query}\r\n").as_bytes()),
priority,
Some(&cancellable.clone()),
move |result| match result {
Ok(_) => Response::from_request_async(
connection,
Some(priority),
Some(cancellable),
move |result| match result {
Ok(response) => callback(Ok(response)),
Err(reason) => callback(Err(Error::Response(reason))),
},
),
Err(reason) => callback(Err(Error::Write(reason))),
},
);
}

82
src/client/connection.rs Normal file
View file

@ -0,0 +1,82 @@
pub mod certificate;
pub mod error;
pub use certificate::Certificate;
pub use error::Error;
use gio::{
prelude::{IOStreamExt, TlsConnectionExt},
IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection,
};
use glib::object::{Cast, IsA};
pub struct Connection {
pub socket_connection: SocketConnection,
pub tls_client_connection: Option<TlsClientConnection>,
}
impl Connection {
// Constructors
/// Create new `Self`
pub fn from(
network_address: NetworkAddress, // @TODO struct cert as sni
socket_connection: SocketConnection,
certificate: Option<TlsCertificate>,
) -> Result<Self, Error> {
if socket_connection.is_closed() {
return Err(Error::Closed);
}
Ok(Self {
socket_connection: socket_connection.clone(),
tls_client_connection: match certificate {
Some(certificate) => match auth(network_address, socket_connection, certificate) {
Ok(tls_client_connection) => Some(tls_client_connection),
Err(reason) => return Err(reason),
},
None => None,
},
})
}
// Getters
pub fn stream(&self) -> impl IsA<IOStream> {
match self.tls_client_connection.clone() {
Some(tls_client_connection) => tls_client_connection.upcast::<IOStream>(),
None => self.socket_connection.clone().upcast::<IOStream>(),
}
}
}
// Tools
pub fn auth(
server_identity: NetworkAddress, // @TODO impl IsA<SocketConnectable> ?
socket_connection: SocketConnection,
certificate: TlsCertificate,
) -> Result<TlsClientConnection, Error> {
if socket_connection.is_closed() {
return Err(Error::Closed);
}
// https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls
match TlsClientConnection::new(&socket_connection, Some(&server_identity)) {
Ok(tls_client_connection) => {
// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates
tls_client_connection.set_certificate(&certificate);
// @TODO handle exceptions
// https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections
tls_client_connection.set_require_close_notify(true);
// @TODO host validation
// https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation
tls_client_connection.connect_accept_certificate(move |_, _, _| true);
Ok(tls_client_connection)
}
Err(reason) => Err(Error::Tls(reason)),
}
}

View file

@ -0,0 +1,52 @@
pub mod error;
pub mod scope;
pub use error::Error;
pub use scope::Scope;
use gio::{prelude::TlsCertificateExt, TlsCertificate};
use glib::DateTime;
pub struct Certificate {
tls_certificate: TlsCertificate,
}
impl Certificate {
// Constructors
/// Create new `Self`
pub fn from_pem(pem: &str) -> Result<Self, Error> {
Ok(Self {
tls_certificate: match TlsCertificate::from_pem(&pem) {
Ok(tls_certificate) => {
// Validate expiration time
match DateTime::now_local() {
Ok(now_local) => {
match tls_certificate.not_valid_after() {
Some(not_valid_after) => {
if now_local > not_valid_after {
return Err(Error::Expired(not_valid_after));
}
}
None => return Err(Error::ValidAfter),
}
match tls_certificate.not_valid_before() {
Some(not_valid_before) => {
if now_local < not_valid_before {
return Err(Error::Inactive(not_valid_before));
}
}
None => return Err(Error::ValidBefore),
}
}
Err(_) => return Err(Error::DateTime),
}
// Success
tls_certificate
}
Err(reason) => return Err(Error::Decode(reason)),
},
})
}
}

View file

@ -0,0 +1,16 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Closed,
Tls(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Closed => write!(f, "Socket connection closed"),
Self::Tls(reason) => write!(f, "Could not create TLS connection: {reason}"),
}
}
}

36
src/client/error.rs Normal file
View file

@ -0,0 +1,36 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Connectable(String),
Connection(super::connection::Error),
Connect(glib::Error),
Request(glib::Error),
Response(super::response::Error),
Write(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Connectable(uri) => {
write!(f, "Could not create connectable address for {uri}")
}
Self::Connection(reason) => {
write!(f, "Connection error: {reason}")
}
Self::Connect(reason) => {
write!(f, "Connect error: {reason}")
}
Self::Request(reason) => {
write!(f, "Request error: {reason}")
}
Self::Response(reason) => {
write!(f, "Response error: {reason}")
}
Self::Write(reason) => {
write!(f, "I/O Write error: {reason}")
}
}
}
}

View file

@ -1,6 +1,35 @@
//! Read and parse Gemini response as Object
pub mod data;
pub mod error;
pub mod meta;
pub use error::Error;
pub use meta::Meta;
use super::Connection;
use gio::Cancellable;
use glib::Priority;
pub struct Response {
pub connection: Connection,
pub meta: Meta,
}
impl Response {
// Constructors
pub fn from_request_async(
connection: Connection,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
callback: impl FnOnce(Result<Self, Error>) + 'static,
) {
Meta::from_stream_async(connection.stream(), priority, cancellable, |result| {
callback(match result {
Ok(meta) => Ok(Self { connection, meta }),
Err(reason) => Err(Error::Meta(reason)),
})
})
}
}

View file

@ -16,7 +16,7 @@ pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M
/// Container for text-based response data
pub struct Text {
data: GString,
pub data: GString,
}
impl Default for Text {
@ -41,10 +41,10 @@ impl Text {
}
/// Create new `Self` from UTF-8 buffer
pub fn from_utf8(buffer: &[u8]) -> Result<Self, (Error, Option<&str>)> {
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
match GString::from_utf8(buffer.into()) {
Ok(data) => Ok(Self::from_string(&data)),
Err(_) => Err((Error::Decode, None)),
Err(reason) => Err(Error::Decode(reason)),
}
}
@ -53,7 +53,7 @@ impl Text {
stream: impl IsA<IOStream>,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
on_complete: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
on_complete: impl FnOnce(Result<Self, Error>) + 'static,
) {
read_all_from_stream_async(
Vec::with_capacity(BUFFER_CAPACITY),
@ -72,13 +72,6 @@ impl Text {
},
);
}
// Getters
/// Get reference to `Self` data
pub fn data(&self) -> &GString {
&self.data
}
}
// Tools
@ -92,7 +85,7 @@ pub fn read_all_from_stream_async(
stream: impl IsA<IOStream>,
cancelable: Option<Cancellable>,
priority: Priority,
callback: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
) {
stream.input_stream().read_bytes_async(
BUFFER_CAPACITY,
@ -107,7 +100,7 @@ pub fn read_all_from_stream_async(
// Validate overflow
if buffer.len() + bytes.len() > BUFFER_MAX_SIZE {
return callback(Err((Error::BufferOverflow, None)));
return callback(Err(Error::BufferOverflow));
}
// Save chunks to buffer
@ -118,7 +111,7 @@ pub fn read_all_from_stream_async(
// Continue bytes reading
read_all_from_stream_async(buffer, stream, cancelable, priority, callback);
}
Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))),
Err(reason) => callback(Err(Error::InputStreamRead(reason))),
},
);
}

View file

@ -1,6 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
BufferOverflow,
Decode,
InputStream,
Decode(std::string::FromUtf8Error),
InputStreamRead(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::BufferOverflow => {
write!(f, "Buffer overflow")
}
Self::Decode(reason) => {
write!(f, "Decode error: {reason}")
}
Self::InputStreamRead(reason) => {
write!(f, "Input stream read error: {reason}")
}
}
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Meta(super::meta::Error),
Stream,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Meta(reason) => {
write!(f, "Meta read error: {reason}")
}
Self::Stream => {
write!(f, "I/O stream error")
}
}
}
}

View file

@ -22,9 +22,9 @@ use glib::{object::IsA, Priority};
pub const MAX_LEN: usize = 0x400; // 1024
pub struct Meta {
status: Status,
data: Option<Data>,
mime: Option<Mime>,
pub status: Status,
pub data: Option<Data>,
pub mime: Option<Mime>,
// @TODO
// charset: Option<Charset>,
// language: Option<Language>,
@ -35,7 +35,7 @@ impl Meta {
/// Create new `Self` from UTF-8 buffer
/// * supports entire response or just meta slice
pub fn from_utf8(buffer: &[u8]) -> Result<Self, (Error, Option<&str>)> {
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> {
// Calculate buffer length once
let len = buffer.len();
@ -46,13 +46,7 @@ impl Meta {
let data = Data::from_utf8(slice);
if let Err(reason) = data {
return Err((
match reason {
data::Error::Decode => Error::DataDecode,
data::Error::Protocol => Error::DataProtocol,
},
None,
));
return Err(Error::Data(reason));
}
// MIME
@ -60,14 +54,7 @@ impl Meta {
let mime = Mime::from_utf8(slice);
if let Err(reason) = mime {
return Err((
match reason {
mime::Error::Decode => Error::MimeDecode,
mime::Error::Protocol => Error::MimeProtocol,
mime::Error::Undefined => Error::MimeUndefined,
},
None,
));
return Err(Error::Mime(reason));
}
// Status
@ -75,14 +62,7 @@ impl Meta {
let status = Status::from_utf8(slice);
if let Err(reason) = status {
return Err((
match reason {
status::Error::Decode => Error::StatusDecode,
status::Error::Protocol => Error::StatusProtocol,
status::Error::Undefined => Error::StatusUndefined,
},
None,
));
return Err(Error::Status(reason));
}
Ok(Self {
@ -91,7 +71,7 @@ impl Meta {
status: status.unwrap(),
})
}
None => Err((Error::Protocol, None)),
None => Err(Error::Protocol),
}
}
@ -100,7 +80,7 @@ impl Meta {
stream: impl IsA<IOStream>,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
on_complete: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
on_complete: impl FnOnce(Result<Self, Error>) + 'static,
) {
read_from_stream_async(
Vec::with_capacity(MAX_LEN),
@ -119,20 +99,6 @@ impl Meta {
},
);
}
// Getters
pub fn status(&self) -> &Status {
&self.status
}
pub fn data(&self) -> &Option<Data> {
&self.data
}
pub fn mime(&self) -> &Option<Mime> {
&self.mime
}
}
// Tools
@ -146,7 +112,7 @@ pub fn read_from_stream_async(
stream: impl IsA<IOStream>,
cancellable: Option<Cancellable>,
priority: Priority,
on_complete: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
on_complete: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
) {
stream.input_stream().read_async(
vec![0],
@ -156,7 +122,7 @@ pub fn read_from_stream_async(
Ok((mut bytes, size)) => {
// Expect valid header length
if size == 0 || buffer.len() >= MAX_LEN {
return on_complete(Err((Error::Protocol, None)));
return on_complete(Err(Error::Protocol));
}
// Read next byte without record
@ -181,7 +147,7 @@ pub fn read_from_stream_async(
// Continue
read_from_stream_async(buffer, stream, cancellable, priority, on_complete);
}
Err((_, reason)) => on_complete(Err((Error::InputStream, Some(reason.message())))),
Err((data, reason)) => on_complete(Err(Error::InputStreamRead(data, reason))),
},
);
}

View file

@ -12,7 +12,7 @@ use glib::GString;
/// * placeholder text for 10, 11 status
/// * URL string for 30, 31 status
pub struct Data {
value: GString,
pub value: GString,
}
impl Data {
@ -52,16 +52,10 @@ impl Data {
false => Some(Self { value }),
true => None,
}),
Err(_) => Err(Error::Decode),
Err(reason) => Err(Error::Decode(reason)),
}
}
None => Err(Error::Protocol),
}
}
// Getters
pub fn value(&self) -> &GString {
&self.value
}
}

View file

@ -1,5 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode,
Decode(std::string::FromUtf8Error),
Protocol,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(reason) => {
write!(f, "Decode error: {reason}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
}
}
}

View file

@ -1,13 +1,33 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
DataDecode,
DataProtocol,
InputStream,
MimeDecode,
MimeProtocol,
MimeUndefined,
Data(super::data::Error),
InputStreamRead(Vec<u8>, glib::Error),
Mime(super::mime::Error),
Protocol,
StatusDecode,
StatusProtocol,
StatusUndefined,
Status(super::status::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Data(reason) => {
write!(f, "Data error: {reason}")
}
Self::InputStreamRead(_, reason) => {
// @TODO
write!(f, "Input stream error: {reason}")
}
Self::Mime(reason) => {
write!(f, "MIME error: {reason}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Status(reason) => {
write!(f, "Status error: {reason}")
}
}
}
}

View file

@ -47,7 +47,7 @@ impl Mime {
match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) {
Some(value) => match GString::from_utf8(value.into()) {
Ok(string) => Self::from_string(string.as_str()),
Err(_) => Err(Error::Decode),
Err(reason) => Err(Error::Decode(reason)),
},
None => Err(Error::Protocol),
}

View file

@ -1,6 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode,
Decode(std::string::FromUtf8Error),
Protocol,
Undefined,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(reason) => {
write!(f, "Decode error: {reason}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Undefined => {
write!(f, "Undefined error")
}
}
}
}

View file

@ -43,7 +43,7 @@ impl Status {
match buffer.get(0..2) {
Some(value) => match GString::from_utf8(value.to_vec()) {
Ok(string) => Self::from_string(string.as_str()),
Err(_) => Err(Error::Decode),
Err(reason) => Err(Error::Decode(reason)),
},
None => Err(Error::Protocol),
}

View file

@ -1,6 +1,24 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Decode,
Decode(std::string::FromUtf8Error),
Protocol,
Undefined,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Decode(reason) => {
write!(f, "Decode error: {reason}")
}
Self::Protocol => {
write!(f, "Protocol error")
}
Self::Undefined => {
write!(f, "Undefined error")
}
}
}
}

View file

@ -1,2 +1,4 @@
pub mod client;
pub mod gio;
pub use client::Client;