begin authorization components implementation for gemini protocol

This commit is contained in:
yggverse 2024-11-16 12:55:30 +02:00
parent b32a1a297f
commit f487215ca9
8 changed files with 319 additions and 34 deletions

View File

@ -20,12 +20,18 @@ use crate::app::browser::{
use crate::Profile; use crate::Profile;
use gtk::{ use gtk::{
gdk_pixbuf::Pixbuf, gdk_pixbuf::Pixbuf,
gio::{Cancellable, SocketClient, SocketClientEvent, SocketProtocol, TlsCertificateFlags}, gio::{
Cancellable, SocketClient, SocketClientEvent, SocketConnectable, SocketProtocol,
TlsCertificate, TlsCertificateFlags, TlsClientConnection,
},
glib::{ glib::{
gformat, Bytes, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri, gformat, Bytes, GString, Priority, Regex, RegexCompileFlags, RegexMatchFlags, Uri,
UriFlags, UriHideFlags, UriFlags, UriHideFlags,
}, },
prelude::{CancellableExt, EditableExt, IOStreamExt, OutputStreamExt, SocketClientExt}, prelude::{
CancellableExt, EditableExt, IOStreamExt, OutputStreamExt, SocketClientExt,
TlsConnectionExt,
},
}; };
use sqlite::Transaction; use sqlite::Transaction;
use std::{cell::RefCell, rc::Rc, time::Duration}; use std::{cell::RefCell, rc::Rc, time::Duration};
@ -434,10 +440,24 @@ impl Page {
// Init socket // Init socket
let client = SocketClient::new(); let client = SocketClient::new();
client.set_protocol(SocketProtocol::Tcp); client.set_protocol(SocketProtocol::Tcp);
// 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_validation_flags(TlsCertificateFlags::INSECURE);
client.set_tls(true); client.set_tls(true);
None
};
// Listen for connection status updates // Listen for connection status updates
client.connect_event({ client.connect_event({
@ -468,6 +488,21 @@ impl Page {
Some(&cancellable.clone()), Some(&cancellable.clone()),
move |connect| match connect { move |connect| match connect {
Ok(connection) => { 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 // Send request
connection.output_stream().write_bytes_async( connection.output_stream().write_bytes_async(
&Bytes::from(gformat!("{url}\r\n").as_bytes()), &Bytes::from(gformat!("{url}\r\n").as_bytes()),

View File

@ -1,10 +1,11 @@
mod bookmark; mod bookmark;
mod database; mod database;
//mod history; //mod history;
//mod identity; mod identity;
use bookmark::Bookmark; use bookmark::Bookmark;
use database::Database; use database::Database;
use identity::Identity;
use gtk::glib::{user_config_dir, DateTime}; use gtk::glib::{user_config_dir, DateTime};
use sqlite::{Connection, Transaction}; use sqlite::{Connection, Transaction};
@ -19,6 +20,7 @@ const DB_NAME: &str = "database.sqlite3";
pub struct Profile { pub struct Profile {
pub bookmark: Rc<Bookmark>, pub bookmark: Rc<Bookmark>,
pub database: Rc<Database>, pub database: Rc<Database>,
pub identity: Rc<Identity>,
pub config_path: PathBuf, pub config_path: PathBuf,
} }
@ -93,7 +95,8 @@ impl Profile {
// Result // Result
Self { 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, database,
config_path, config_path,
} }
@ -108,9 +111,9 @@ pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Delegate migration to children components // Delegate migration to children components
bookmark::migrate(tx)?; bookmark::migrate(tx)?;
identity::migrate(tx)?;
// @TODO not in use yet // @TODO not in use yet
// history::migrate(tx)?; // history::migrate(tx)?;
// identity::migrate(tx)?;
// Success // Success
Ok(()) Ok(())

View File

@ -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<Gemini>,
}
impl Identity {
// Constructors
/// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> 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<String> {
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 // Tools
pub fn migrate(tx: &Transaction) -> Result<(), String> { pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Migrate self components // Migrate self components
if let Err(e) = database::init(tx) { // nothing yet..
return Err(e.to_string());
}
// Delegate migration to childs // Delegate migration to childs
// nothing yet.. gemini::migrate(tx)?;
// Success // Success
Ok(()) Ok(())

View File

@ -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<usize, Error> {
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
)",
[],
)
}

View File

@ -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<Auth>,
pub database: Rc<Database>,
}
impl Gemini {
// Constructors
/// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> 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(())
}

View File

@ -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<Database>,
}
impl Auth {
// Constructors
/// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> 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(())
}

View File

@ -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<RwLock<Connection>>,
profile_id: Rc<i64>, // multi-profile relationship
}
impl Database {
// Constructors
/// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> 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<Vec<Table>, 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<usize, Error> {
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<Vec<Table>, 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)
}

View File

@ -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<RwLock<Connection>>,
profile_id: Rc<i64>, // multi-profile relationship
}
impl Database {
// Constructors
/// Create new `Self`
pub fn new(connection: Rc<RwLock<Connection>>, profile_id: Rc<i64>) -> Self {
Self {
connection,
profile_id,
}
}
/// Get all records match current `profile_id`
pub fn records(&self) -> Result<Vec<Table>, 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<usize, Error> {
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<Vec<Table>, 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)
}