From 4c305f967f3cf3b814706251577128a870422d09 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 24 Jul 2025 06:25:01 +0300 Subject: [PATCH] implement proxy backend features --- README.md | 5 +- .../window/tab/item/client/driver/gemini.rs | 5 + .../window/tab/item/client/driver/nex.rs | 1 + src/profile.rs | 6 + src/profile/proxy.rs | 101 ++++++++++++ src/profile/proxy/database.rs | 146 ++++++++++++++++++ src/profile/proxy/database/ignore.rs | 4 + src/profile/proxy/database/rule.rs | 5 + src/profile/proxy/ignore.rs | 4 + src/profile/proxy/rule.rs | 5 + 10 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/profile/proxy.rs create mode 100644 src/profile/proxy/database.rs create mode 100644 src/profile/proxy/database/ignore.rs create mode 100644 src/profile/proxy/database/rule.rs create mode 100644 src/profile/proxy/ignore.rs create mode 100644 src/profile/proxy/rule.rs diff --git a/README.md b/README.md index dcd98186..c62d33e3 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,10 @@ GTK 4 / Libadwaita client written in Rust * [x] Page navigation * [x] Recently visited * [x] Recently closed -* [ ] Proxy +* [ ] Proxy (by [SimpleProxyResolver](https://docs.gtk.org/gio/class.SimpleProxyResolver.html)) + * [x] Multiple regex rules by the priority + * [x] Custom ignored hosts + * [ ] UI controls (frontend) * [ ] Session * [ ] Window * [x] Size diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index f9d6dde5..3c48917b 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -98,6 +98,11 @@ impl Gemini { is_snap_history: bool, ) { use ggemini::client::connection::request::{Mode, Request}; + + self.client + .socket + .set_proxy_resolver(self.page.profile.proxy.matches(&uri).as_ref()); + match uri.scheme().as_str() { "gemini" => handle( self, diff --git a/src/app/browser/window/tab/item/client/driver/nex.rs b/src/app/browser/window/tab/item/client/driver/nex.rs index a10b9797..81a41c40 100644 --- a/src/app/browser/window/tab/item/client/driver/nex.rs +++ b/src/app/browser/window/tab/item/client/driver/nex.rs @@ -67,6 +67,7 @@ impl Nex { } let socket = SocketClient::new(); + socket.set_proxy_resolver(self.page.profile.proxy.matches(&uri).as_ref()); socket.set_protocol(SocketProtocol::Tcp); socket.set_timeout(30); // @TODO optional diff --git a/src/profile.rs b/src/profile.rs index e3bc3378..54eb6872 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -2,6 +2,7 @@ mod bookmark; mod database; mod history; mod identity; +mod proxy; mod search; mod tofu; @@ -11,6 +12,7 @@ use database::Database; use gtk::glib::{DateTime, user_config_dir}; use history::History; use identity::Identity; +use proxy::Proxy; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use search::Search; @@ -30,6 +32,7 @@ pub struct Profile { pub database: Database, pub history: History, pub identity: Identity, + pub proxy: Proxy, pub search: Search, pub tofu: Tofu, } @@ -86,6 +89,7 @@ impl Profile { let bookmark = Bookmark::build(&database_pool, profile_id)?; let history = History::build(&database_pool, profile_id)?; let identity = Identity::build(&database_pool, profile_id)?; + let proxy = Proxy::init(&database_pool, profile_id)?; let search = Search::build(&database_pool, profile_id)?; let tofu = Tofu::init(&database_pool, profile_id)?; @@ -96,6 +100,7 @@ impl Profile { database, history, identity, + proxy, search, tofu, }) @@ -118,6 +123,7 @@ pub fn migrate(tx: &Transaction) -> Result<()> { bookmark::migrate(tx)?; history::migrate(tx)?; identity::migrate(tx)?; + proxy::migrate(tx)?; search::migrate(tx)?; tofu::migrate(tx)?; diff --git a/src/profile/proxy.rs b/src/profile/proxy.rs new file mode 100644 index 00000000..7a06f52f --- /dev/null +++ b/src/profile/proxy.rs @@ -0,0 +1,101 @@ +mod database; +mod ignore; +mod rule; + +use anyhow::Result; +use database::Database; +use gtk::{ + gio::{ProxyResolver, SimpleProxyResolver}, + glib::Uri, +}; +use ignore::Ignore; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; +use rule::Rule; +use std::cell::RefCell; + +pub struct Proxy { + ignore: RefCell>, + rule: RefCell>, +} + +impl Proxy { + // Constructors + + pub fn init(database_pool: &Pool, profile_id: i64) -> Result { + let database = Database::init(database_pool, profile_id); + + let ignores = database.ignores()?; + let ignore = RefCell::new(Vec::with_capacity(ignores.len())); + + { + // build in-memory index... + let mut b = ignore.borrow_mut(); + for i in ignores { + b.push(Ignore { + is_enabled: i.is_enabled, + host: i.host, + }); + } + } + + let rules = database.rules()?; + let rule = RefCell::new(Vec::with_capacity(rules.len())); + + { + // build in-memory index... + let mut b = rule.borrow_mut(); + for r in rules { + b.push(Rule { + is_enabled: r.is_enabled, + regex: r.regex, + url: r.url, + }); + } + } + + Ok(Self { ignore, rule }) + } + + // Actions + + pub fn matches(&self, request: &Uri) -> Option { + for rule in self.rule.borrow().iter().filter(|r| r.is_enabled) { + if gtk::glib::Regex::match_simple( + &rule.regex, + request.to_str(), + gtk::glib::RegexCompileFlags::DEFAULT, + gtk::glib::RegexMatchFlags::DEFAULT, + ) { + return Some(SimpleProxyResolver::new( + Some(&rule.url), + self.ignore + .borrow() + .iter() + .filter_map(|i| { + if i.is_enabled { + Some(i.host.clone()) + } else { + None + } + }) + .collect::>(), + )); + } + } + None + } +} + +// Tools + +pub fn migrate(tx: &sqlite::Transaction) -> Result<()> { + // Migrate self components + database::init(tx)?; + + // Delegate migration to childs + // nothing yet... + + // Success + Ok(()) +} diff --git a/src/profile/proxy/database.rs b/src/profile/proxy/database.rs new file mode 100644 index 00000000..05ecbe5e --- /dev/null +++ b/src/profile/proxy/database.rs @@ -0,0 +1,146 @@ +mod ignore; +mod rule; + +use anyhow::Result; +use ignore::Ignore; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; +use rule::Rule; +use sqlite::Transaction; + +pub struct Database { + pool: Pool, + profile_id: i64, +} + +impl Database { + // Constructors + + pub fn init(pool: &Pool, profile_id: i64) -> Self { + Self { + pool: pool.clone(), + profile_id, + } + } + + // Getters + + pub fn rules(&self) -> Result> { + rules(&self.pool.get()?.unchecked_transaction()?, self.profile_id) + } + + pub fn ignores(&self) -> Result> { + ignores(&self.pool.get()?.unchecked_transaction()?, self.profile_id) + } + + // Setters +} + +// Low-level DB API + +pub fn init(tx: &Transaction) -> Result { + let mut s = 0; + + s += tx.execute( + "CREATE TABLE IF NOT EXISTS `profile_proxy_ignore` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_id` INTEGER NOT NULL, + `time` INTEGER NOT NULL, + `is_enabled` INTEGER NOT NULL, + `host` VARCHAR(255) NOT NULL, + + FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`), + UNIQUE (`host`) + )", + [], + )?; + + s += tx.execute( + "CREATE TABLE IF NOT EXISTS `profile_proxy_rule` + ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `profile_id` INTEGER NOT NULL, + `time` INTEGER NOT NULL, + `is_enabled` INTEGER NOT NULL, + `priority` INTEGER NOT NULL, + `regex` VARCHAR(255) NOT NULL, + `url` VARCHAR(255) NOT NULL, + + FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`), + UNIQUE (`regex`) + )", + [], + )?; + + Ok(s) +} + +pub fn ignores(tx: &Transaction, profile_id: i64) -> Result> { + let mut stmt = tx.prepare( + "SELECT `id`, + `profile_id`, + `time`, + `host`, + `is_enabled` + + FROM `profile_proxy_ignore` + WHERE `profile_id` = ?", + )?; + + let result = stmt.query_map([profile_id], |row| { + Ok(Ignore { + //id: row.get(0)?, + //profile_id: row.get(1)?, + //time: DateTime::from_unix_local(row.get(2)?).unwrap(), + host: row.get(3)?, + is_enabled: row.get(4)?, + }) + })?; + + let mut records = Vec::new(); + + for record in result { + let table = record?; + records.push(table); + } + + Ok(records) +} + +pub fn rules(tx: &Transaction, profile_id: i64) -> Result> { + let mut stmt = tx.prepare( + "SELECT `id`, + `profile_id`, + `time`, + `is_enabled`, + `priority`, + `regex`, + `url` + + FROM `profile_proxy_rule` + WHERE `profile_id` = ? + ORDER BY `priority` ASC", + )?; + + let result = stmt.query_map([profile_id], |row| { + Ok(Rule { + //id: row.get(0)?, + //profile_id: row.get(1)?, + //time: DateTime::from_unix_local(row.get(2)?).unwrap(), + is_enabled: row.get(3)?, + //priority: row.get(4)?, + regex: row.get(5)?, + url: row.get(6)?, + }) + })?; + + let mut records = Vec::new(); + + for record in result { + let table = record?; + records.push(table); + } + + Ok(records) +} diff --git a/src/profile/proxy/database/ignore.rs b/src/profile/proxy/database/ignore.rs new file mode 100644 index 00000000..bb7746a1 --- /dev/null +++ b/src/profile/proxy/database/ignore.rs @@ -0,0 +1,4 @@ +pub struct Ignore { + pub host: String, + pub is_enabled: bool, +} diff --git a/src/profile/proxy/database/rule.rs b/src/profile/proxy/database/rule.rs new file mode 100644 index 00000000..86355df5 --- /dev/null +++ b/src/profile/proxy/database/rule.rs @@ -0,0 +1,5 @@ +pub struct Rule { + pub is_enabled: bool, + pub regex: String, + pub url: String, +} diff --git a/src/profile/proxy/ignore.rs b/src/profile/proxy/ignore.rs new file mode 100644 index 00000000..91981d89 --- /dev/null +++ b/src/profile/proxy/ignore.rs @@ -0,0 +1,4 @@ +pub struct Ignore { + pub is_enabled: bool, + pub host: String, +} diff --git a/src/profile/proxy/rule.rs b/src/profile/proxy/rule.rs new file mode 100644 index 00000000..86355df5 --- /dev/null +++ b/src/profile/proxy/rule.rs @@ -0,0 +1,5 @@ +pub struct Rule { + pub is_enabled: bool, + pub regex: String, + pub url: String, +}