diff --git a/Cargo.toml b/Cargo.toml index 442d0ee..801fb2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.0" +version = "0.20.1" edition = "2024" license = "MIT" readme = "README.md" @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.9" +version = "0.21.0" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.9" +version = "0.21.0" features = ["v2_66"] diff --git a/README.md b/README.md index d81850e..03c633e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ fn main() -> ExitCode { }, Priority::DEFAULT, Cancellable::new(), - None, // optional `GTlsCertificate` + None, // optional auth `GTlsCertificate` + None, // optional TOFU `GTlsCertificate` array |result| match result { Ok((response, _connection)) => match response { Response::Success(success) => match success.mime().unwrap().as_str() { diff --git a/src/client.rs b/src/client.rs index cff557e..2152781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -59,7 +59,8 @@ impl Client { request: Request, priority: Priority, cancellable: Cancellable, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection @@ -73,30 +74,33 @@ impl Client { move |result| match result { Ok(socket_connection) => { match Connection::build( - socket_connection, + socket_connection.clone(), network_address, - certificate, + client_certificate, + server_certificates, is_session_resumption, ) { - Ok(connection) => connection.request_async( + Ok(connection) => connection.clone().request_async( request, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Connection(e)), + Err(e) => Err(Error::Request(connection, e)), }) }, ), - Err(e) => callback(Err(Error::Connection(e))), + Err(e) => { + callback(Err(Error::Connection(socket_connection, e))) + } } } - Err(e) => callback(Err(Error::Connect(e))), + Err(e) => callback(Err(Error::Connect(network_address, e))), } }) } - Err(e) => callback(Err(Error::Request(e))), + Err(e) => callback(Err(Error::NetworkAddress(e))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index d1cd849..6be90f1 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,17 +6,16 @@ pub use error::Error; pub use request::{Mode, Request}; pub use response::Response; -// Local dependencies - use gio::{ Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt}, }; use glib::{ Bytes, Priority, object::{Cast, ObjectExt}, }; +#[derive(Debug, Clone)] pub struct Connection { pub network_address: NetworkAddress, pub socket_connection: SocketConnection, @@ -30,17 +29,19 @@ impl Connection { pub fn build( socket_connection: SocketConnection, network_address: NetworkAddress, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, Some(&network_address), + server_certificates, is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref c) = certificate { + if let Some(ref c) = client_certificate { tls_client_connection.set_certificate(c); } tls_client_connection @@ -136,6 +137,7 @@ impl Connection { fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { @@ -149,9 +151,19 @@ fn new_tls_client_connection( // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); - // @TODO validate - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(|_, _, _| true); + // [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation) + tls_client_connection.connect_accept_certificate(move |_, c, _| { + server_certificates + .as_ref() + .is_none_or(|server_certificates| { + for server_certificate in server_certificates { + if server_certificate.is_same(c) { + return true; + } + } + false + }) + }); Ok(tls_client_connection) } diff --git a/src/client/error.rs b/src/client/error.rs index 6083e77..73031da 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,22 +2,29 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Connect(glib::Error), - Connection(crate::client::connection::Error), - Request(crate::client::connection::request::Error), + Connect(gio::NetworkAddress, glib::Error), + Connection(gio::SocketConnection, crate::client::connection::Error), + NetworkAddress(crate::client::connection::request::Error), + Request( + crate::client::connection::Connection, + crate::client::connection::Error, + ), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connection(e) => { - write!(f, "Connection error: {e}") - } - Self::Connect(e) => { + Self::Connect(_, e) => { write!(f, "Connect error: {e}") } - Self::Request(e) => { - write!(f, "Request error: {e}") + Self::Connection(_, e) => { + write!(f, "Connection init error: {e}") + } + Self::NetworkAddress(e) => { + write!(f, "Network address error: {e}") + } + Self::Request(_, e) => { + write!(f, "Connection error: {e}") } } } diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index a8e5d70..2dffb5e 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -33,13 +33,13 @@ pub fn from_stream_async( size.total += bytes.len(); on_chunk(bytes.clone(), size.total); - if let Some(limit) = size.limit { - if size.total > limit { - return on_complete(Err(Error::BytesTotal(size.total, limit))); - } + if let Some(limit) = size.limit + && size.total > limit + { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } - if bytes.len() == 0 { + if bytes.is_empty() { return on_complete(Ok((file_output_stream, size.total))); } diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs index 285d9f1..5d0c911 100644 --- a/src/gio/file_output_stream/size.rs +++ b/src/gio/file_output_stream/size.rs @@ -5,3 +5,13 @@ pub struct Size { pub limit: Option, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: None, + total: 0, + } + } +} diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs index b95ef39..9a10bd3 100644 --- a/src/gio/memory_input_stream/size.rs +++ b/src/gio/memory_input_stream/size.rs @@ -4,3 +4,13 @@ pub struct Size { pub limit: usize, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: 0xfffff, // 1 MB + total: 0, + } + } +}