From f487215ca919444a6b04eddc4995b0b88ab280fe Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 16 Nov 2024 12:55:30 +0200 Subject: [PATCH] begin authorization components implementation for gemini protocol --- src/app/browser/window/tab/item/page.rs | 45 +++++++++-- src/profile.rs | 9 ++- src/profile/identity.rs | 46 +++++++++-- src/profile/identity/database.rs | 20 ----- src/profile/identity/gemini.rs | 45 +++++++++++ src/profile/identity/gemini/auth.rs | 37 +++++++++ src/profile/identity/gemini/auth/database.rs | 81 ++++++++++++++++++++ src/profile/identity/gemini/database.rs | 70 +++++++++++++++++ 8 files changed, 319 insertions(+), 34 deletions(-) delete mode 100644 src/profile/identity/database.rs create mode 100644 src/profile/identity/gemini.rs create mode 100644 src/profile/identity/gemini/auth.rs create mode 100644 src/profile/identity/gemini/auth/database.rs create mode 100644 src/profile/identity/gemini/database.rs diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 2ca10835..e843cbb7 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -20,12 +20,18 @@ use crate::app::browser::{ use crate::Profile; use gtk::{ gdk_pixbuf::Pixbuf, - gio::{Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificateFlags}, + gio::{ + Cancellable, SocketClient, SocketClientEvent, SocketConnectable, SocketProtocol, + TlsCertificate, TlsCertificateFlags, TlsClientConnection, + }, glib::{ gformat, Bytes, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, UriFlags, UriHideFlags, }, - prelude::{CancellableExt, EditableExt, IOStreamExt, OutputStreamExt, SocketClientExt}, + prelude::{ + CancellableExt, EditableExt, IOStreamExt, OutputStreamExt, SocketClientExt, + TlsConnectionExt, + }, }; use sqlite::Transaction; use std::{cell::RefCell, rc::Rc, time::Duration}; @@ -434,10 +440,24 @@ impl Page { // Init socket let client = SocketClient::new(); - client.set_protocol(SocketProtocol::Tcp); - client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); - client.set_tls(true); + + // Check request match configured identity in profile database + let auth = if let Some(pem) = self + .profile + .identity + .gemini(&self.navigation.request().widget().gobject().text()) + { + match TlsCertificate::from_pem(&pem) { + Ok(certificate) => Some(certificate), + Err(_) => todo!(), + } + } else { + // Use unauthorized connection + client.set_tls_validation_flags(TlsCertificateFlags::INSECURE); + client.set_tls(true); + None + }; // Listen for connection status updates client.connect_event({ @@ -468,6 +488,21 @@ impl Page { Some(&cancellable.clone()), move |connect| match connect { Ok(connection) => { + // Wrap connection with TLS + if let Some(certificate) = auth { + let connection = TlsClientConnection::new( + &connection.clone(), + None::<&SocketConnectable>, + ) + .unwrap(); // @TODO handle + // Apply authorization + connection.set_certificate( + &certificate, + ); + // Validate @TODO + // connection.connect_accept_certificate(|_, _, _| true); + } + // Send request connection.output_stream().write_bytes_async( &Bytes::from(gformat!("{url}\r\n").as_bytes()), diff --git a/src/profile.rs b/src/profile.rs index 038b87e6..34eb41dc 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,10 +1,11 @@ mod bookmark; mod database; //mod history; -//mod identity; +mod identity; use bookmark::Bookmark; use database::Database; +use identity::Identity; use gtk::glib::{user_config_dir, DateTime}; use sqlite::{Connection, Transaction}; @@ -19,6 +20,7 @@ const DB_NAME: &str = "database.sqlite3"; pub struct Profile { pub bookmark: Rc, pub database: Rc, + pub identity: Rc, pub config_path: PathBuf, } @@ -93,7 +95,8 @@ impl Profile { // Result Self { - bookmark: Rc::new(Bookmark::new(connection, profile_id)), + bookmark: Rc::new(Bookmark::new(connection.clone(), profile_id.clone())), + identity: Rc::new(Identity::new(connection, profile_id)), database, config_path, } @@ -108,9 +111,9 @@ pub fn migrate(tx: &Transaction) -> Result<(), String> { // Delegate migration to children components bookmark::migrate(tx)?; + identity::migrate(tx)?; // @TODO not in use yet // history::migrate(tx)?; - // identity::migrate(tx)?; // Success Ok(()) diff --git a/src/profile/identity.rs b/src/profile/identity.rs index ac00d38d..e3068b9c 100644 --- a/src/profile/identity.rs +++ b/src/profile/identity.rs @@ -1,17 +1,51 @@ -mod database; +mod gemini; +use gemini::Gemini; -use sqlite::Transaction; +use sqlite::{Connection, Transaction}; +use std::{rc::Rc, sync::RwLock}; + +/// Authorization wrapper for different protocols +pub struct Identity { + gemini: Rc, +} + +impl Identity { + // Constructors + + /// Create new `Self` + pub fn new(connection: Rc>, profile_id: Rc) -> Self { + Self { + gemini: Rc::new(Gemini::new(connection, profile_id)), + } + } + + /// Get `pem` record match `request` + /// + /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates + pub fn gemini(&self, request: &str) -> Option { + if let Ok(auth_records) = self.gemini.auth.database.records(Some(request)) { + for auth_record in auth_records { + if let Ok(gemini_records) = self.gemini.database.records() { + for gemini_record in gemini_records { + if gemini_record.id == auth_record.gemini_id { + return Some(gemini_record.pem); + } + } + } + } + } + None + } // @TODO apply protocol rules to selection +} // Tools pub fn migrate(tx: &Transaction) -> Result<(), String> { // Migrate self components - if let Err(e) = database::init(tx) { - return Err(e.to_string()); - } + // nothing yet.. // Delegate migration to childs - // nothing yet.. + gemini::migrate(tx)?; // Success Ok(()) diff --git a/src/profile/identity/database.rs b/src/profile/identity/database.rs deleted file mode 100644 index 105aeab5..00000000 --- a/src/profile/identity/database.rs +++ /dev/null @@ -1,20 +0,0 @@ -use sqlite::{Error, Transaction}; - -pub struct Table { - pub id: i64, - //pub profile_id: i64, -} - -// Low-level DB API - -pub fn init(tx: &Transaction) -> Result { - tx.execute( - "CREATE TABLE IF NOT EXISTS `profile_identity` - ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `profile_id` INTEGER NOT NULL, - `pem` TEXT NOT NULL - )", - [], - ) -} diff --git a/src/profile/identity/gemini.rs b/src/profile/identity/gemini.rs new file mode 100644 index 00000000..51e36670 --- /dev/null +++ b/src/profile/identity/gemini.rs @@ -0,0 +1,45 @@ +mod auth; +mod database; + +use auth::Auth; +use database::Database; + +use sqlite::{Connection, Transaction}; +use std::{rc::Rc, sync::RwLock}; + +/// Authorization wrapper for Gemini protocol +/// +/// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates +pub struct Gemini { + pub auth: Rc, + pub database: Rc, +} + +impl Gemini { + // Constructors + + /// Create new `Self` + pub fn new(connection: Rc>, profile_id: Rc) -> Self { + Self { + auth: Rc::new(Auth::new(connection.clone(), profile_id.clone())), + database: Rc::new(Database::new(connection, profile_id)), + } + } + + // @TODO create new identity API +} + +// Tools + +pub fn migrate(tx: &Transaction) -> Result<(), String> { + // Migrate self components + if let Err(e) = database::init(tx) { + return Err(e.to_string()); + } + + // Delegate migration to childs + auth::migrate(tx)?; + + // Success + Ok(()) +} diff --git a/src/profile/identity/gemini/auth.rs b/src/profile/identity/gemini/auth.rs new file mode 100644 index 00000000..c409cda6 --- /dev/null +++ b/src/profile/identity/gemini/auth.rs @@ -0,0 +1,37 @@ +mod database; + +use database::Database; + +use sqlite::{Connection, Transaction}; +use std::{rc::Rc, sync::RwLock}; + +/// API for `gemini_id` + `request` auth pairs operations +pub struct Auth { + pub database: Rc, +} + +impl Auth { + // Constructors + + /// Create new `Self` + pub fn new(connection: Rc>, profile_id: Rc) -> Self { + Self { + database: Rc::new(Database::new(connection, profile_id)), + } + } +} + +// Tools + +pub fn migrate(tx: &Transaction) -> Result<(), String> { + // Migrate self components + if let Err(e) = database::init(tx) { + return Err(e.to_string()); + } + + // Delegate migration to childs + // nothing yet.. + + // Success + Ok(()) +} diff --git a/src/profile/identity/gemini/auth/database.rs b/src/profile/identity/gemini/auth/database.rs new file mode 100644 index 00000000..299ccfe6 --- /dev/null +++ b/src/profile/identity/gemini/auth/database.rs @@ -0,0 +1,81 @@ +use std::{rc::Rc, sync::RwLock}; + +use sqlite::{Connection, Error, Transaction}; + +pub struct Table { + //pub id: i64, + //pub profile_id: i64, + pub gemini_id: i64, +} + +/// Storage for `gemini_id` + `request` auth pairs +pub struct Database { + connection: Rc>, + profile_id: Rc, // multi-profile relationship +} + +impl Database { + // Constructors + + /// Create new `Self` + pub fn new(connection: Rc>, profile_id: Rc) -> Self { + Self { + connection, + profile_id, + } + } + + // Getters + + /// Get records from database match current `profile_id` optionally filtered by `request` + pub fn records(&self, request: Option<&str>) -> Result, Error> { + let readable = self.connection.read().unwrap(); // @TODO + let tx = readable.unchecked_transaction()?; + select(&tx, *self.profile_id, request) + } +} + +// Low-level DB API + +pub fn init(tx: &Transaction) -> Result { + tx.execute( + "CREATE TABLE IF NOT EXISTS `profile_identity_gemini_auth` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_id` INTEGER NOT NULL, + `gemini_id` INTEGER NOT NULL, + `request` TEXT NOT NULL + )", + [], + ) +} + +pub fn select( + tx: &Transaction, + profile_id: i64, + request: Option<&str>, +) -> Result, Error> { + let mut stmt = tx.prepare( + "SELECT `id`, + `profile_id`, + `gemini_id` FROM `profile_identity_gemini_auth` + WHERE `profile_id` = ? AND `request` LIKE ?", + )?; + + let result = stmt.query_map((profile_id, request.unwrap_or("%")), |row| { + Ok(Table { + //id: row.get(0)?, + //profile_id: row.get(1)?, + gemini_id: row.get(2)?, + }) + })?; + + let mut records = Vec::new(); + + for record in result { + let table = record?; + records.push(table); + } + + Ok(records) +} diff --git a/src/profile/identity/gemini/database.rs b/src/profile/identity/gemini/database.rs new file mode 100644 index 00000000..ea29d12a --- /dev/null +++ b/src/profile/identity/gemini/database.rs @@ -0,0 +1,70 @@ +use sqlite::{Connection, Error, Transaction}; +use std::{rc::Rc, sync::RwLock}; + +pub struct Table { + pub id: i64, + //pub profile_id: i64, + pub pem: String, +} + +/// Storage for Gemini auth certificates +pub struct Database { + connection: Rc>, + profile_id: Rc, // multi-profile relationship +} + +impl Database { + // Constructors + + /// Create new `Self` + pub fn new(connection: Rc>, profile_id: Rc) -> Self { + Self { + connection, + profile_id, + } + } + + /// Get all records match current `profile_id` + pub fn records(&self) -> Result, Error> { + let readable = self.connection.read().unwrap(); // @TODO + let tx = readable.unchecked_transaction()?; + select(&tx, *self.profile_id) + } +} + +// Low-level DB API + +pub fn init(tx: &Transaction) -> Result { + tx.execute( + "CREATE TABLE IF NOT EXISTS `profile_identity_gemini` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_id` INTEGER NOT NULL, + `pem` TEXT NOT NULL + )", + [], + ) +} + +pub fn select(tx: &Transaction, profile_id: i64) -> Result, Error> { + let mut stmt = tx.prepare( + "SELECT `id`, `profile_id`, `pem` FROM `profile_identity_gemini` WHERE `profile_id` = ?", + )?; + + let result = stmt.query_map([profile_id], |row| { + Ok(Table { + id: row.get(0)?, + //profile_id: row.get(1)?, + pem: row.get(2)?, + }) + })?; + + let mut records = Vec::new(); + + for record in result { + let table = record?; + records.push(table); + } + + Ok(records) +}