From 4e712260ff63a56d1c823ebf474ddcbdcf39de9a Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 27 Nov 2024 18:03:22 +0200 Subject: [PATCH] add client certificate api --- src/client.rs | 2 + src/client/{connection => }/certificate.rs | 9 +++- src/client/certificate/error.rs | 55 ++++++++++++++++++++++ src/client/certificate/scope.rs | 41 ++++++++++++++++ src/client/certificate/scope/error.rs | 24 ++++++++++ src/client/connection.rs | 3 -- 6 files changed, 129 insertions(+), 5 deletions(-) rename src/client/{connection => }/certificate.rs (84%) create mode 100644 src/client/certificate/error.rs create mode 100644 src/client/certificate/scope.rs create mode 100644 src/client/certificate/scope/error.rs diff --git a/src/client.rs b/src/client.rs index ea35202..89ef9c7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,12 @@ //! High-level client API to interact with Gemini Socket Server: //! * https://geminiprotocol.net/docs/protocol-specification.gmi +pub mod certificate; pub mod connection; pub mod error; pub mod response; +pub use certificate::Certificate; pub use connection::Connection; pub use error::Error; pub use response::Response; diff --git a/src/client/connection/certificate.rs b/src/client/certificate.rs similarity index 84% rename from src/client/connection/certificate.rs rename to src/client/certificate.rs index f33e2f4..11e8ec1 100644 --- a/src/client/connection/certificate.rs +++ b/src/client/certificate.rs @@ -8,15 +8,20 @@ use gio::{prelude::TlsCertificateExt, TlsCertificate}; use glib::DateTime; pub struct Certificate { - tls_certificate: TlsCertificate, + pub scope: Scope, + pub tls_certificate: TlsCertificate, } impl Certificate { // Constructors /// Create new `Self` - pub fn from_pem(pem: &str) -> Result { + pub fn from_pem(pem: &str, scope_url: &str) -> Result { Ok(Self { + scope: match Scope::from_url(scope_url) { + Ok(scope) => scope, + Err(reason) => return Err(Error::Scope(reason)), + }, tls_certificate: match TlsCertificate::from_pem(&pem) { Ok(tls_certificate) => { // Validate expiration time diff --git a/src/client/certificate/error.rs b/src/client/certificate/error.rs new file mode 100644 index 0000000..5dae782 --- /dev/null +++ b/src/client/certificate/error.rs @@ -0,0 +1,55 @@ +use std::fmt::{Display, Formatter, Result}; + +use glib::gformat; + +#[derive(Debug)] +pub enum Error { + DateTime, + Decode(glib::Error), + Expired(glib::DateTime), + Inactive(glib::DateTime), + Scope(crate::client::certificate::scope::Error), + ValidAfter, + ValidBefore, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::DateTime => { + write!(f, "Could not parse local `DateTime`") + } + Self::Decode(reason) => { + write!( + f, + "Could not decode TLS certificate from PEM string: {reason}" + ) + } + Self::Expired(not_valid_after) => { + write!( + f, + "Certificate expired after: {}", + match not_valid_after.format_iso8601() { + Ok(value) => value, + Err(_) => gformat!("unknown"), + } + ) + } + Self::Inactive(not_valid_before) => { + write!( + f, + "Certificate inactive before: {}", + match not_valid_before.format_iso8601() { + Ok(value) => value, + Err(_) => gformat!("unknown"), + } + ) + } + Self::Scope(reason) => { + write!(f, "Certificate inactive before: {reason}") + } + Self::ValidAfter => write!(f, "Could not get `not_valid_after` value"), + Self::ValidBefore => write!(f, "Could not get `not_valid_before` value"), + } + } +} diff --git a/src/client/certificate/scope.rs b/src/client/certificate/scope.rs new file mode 100644 index 0000000..b50b051 --- /dev/null +++ b/src/client/certificate/scope.rs @@ -0,0 +1,41 @@ +pub mod error; +pub use error::Error; + +use glib::{GString, Uri, UriFlags, UriHideFlags}; + +/// Scope implement path prefix to apply TLS authorization for +/// * https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 +pub struct Scope { + uri: Uri, +} + +impl Scope { + // Constructors + + /// Create new `Self` for given `url` string + /// * check URI parts required for valid `Scope` build + pub fn from_url(url: &str) -> Result { + match Uri::parse(url, UriFlags::NONE) { + Ok(uri) => { + if !uri.scheme().to_lowercase().contains("gemini") { + return Err(Error::Scheme); + } + + if uri.host().is_none() { + return Err(Error::Host); + } + + Ok(Self { uri }) + } + Err(reason) => Err(Error::Uri(reason)), + } + } + + // Getters + + /// Get `Scope` string match [Specification](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) + pub fn to_string(&self) -> GString { + self.uri + .to_string_partial(UriHideFlags::QUERY | UriHideFlags::FRAGMENT) + } +} diff --git a/src/client/certificate/scope/error.rs b/src/client/certificate/scope/error.rs new file mode 100644 index 0000000..ea32f87 --- /dev/null +++ b/src/client/certificate/scope/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Host, + Scheme, + Uri(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Host => { + write!(f, "Host required") + } + Self::Scheme => { + write!(f, "Scope does not match `gemini`") + } + Self::Uri(reason) => { + write!(f, "Could not parse URI: {reason}") + } + } + } +} diff --git a/src/client/connection.rs b/src/client/connection.rs index 1dede2a..09f5bda 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,7 +1,4 @@ -pub mod certificate; pub mod error; - -pub use certificate::Certificate; pub use error::Error; use gio::{