From df8dea9534bc188e8915d934807899f756245931 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 20 Jan 2025 07:12:38 +0200 Subject: [PATCH] begin request reorganization with isolated driver imp --- src/app/browser/window/tab/item/page.rs | 58 +-- .../browser/window/tab/item/page/client.rs | 9 +- .../window/tab/item/page/client/request.rs | 92 +---- .../tab/item/page/client/request/gemini.rs | 370 +++++++++--------- .../window/tab/item/page/client/response.rs | 8 +- .../tab/item/page/client/response/redirect.rs | 6 +- .../tab/item/page/client/response/text.rs | 6 + 7 files changed, 261 insertions(+), 288 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/client/response/text.rs diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 254409d3..064d3399 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -359,7 +359,7 @@ fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) { /// * may call itself on Titan response fn handle(page: &Rc, response: client::Response) { use client::{ - response::{Certificate, Failure, Input, Redirect}, + response::{text::Text, Certificate, Failure, Input, Redirect}, Response, }; match response { @@ -466,43 +466,43 @@ fn handle(page: &Rc, response: client::Response) { page.browser_action.update.activate(Some(&page.id)); } Response::Redirect(this) => match this { - Redirect::Background(request) => { - load(&page, Some(&request.as_uri().to_string()), false) - } - Redirect::Foreground(request) => { + Redirect::Background(uri) => load(&page, Some(&uri.to_string()), false), + Redirect::Foreground(uri) => { page.navigation .request .widget .entry - .set_text(&request.as_uri().to_string()); - load(&page, Some(&request.as_uri().to_string()), false); + .set_text(&uri.to_string()); + load(&page, Some(&uri.to_string()), false); } }, - Response::TextGemini { - base, - source, - is_source_request, - } => { - let widget = if is_source_request { - page.content.to_text_source(&source) - } else { - page.content.to_text_gemini(&base, &source) - }; + Response::Text(this) => match this { + Text::Gemini { base, data } => { + /* @TODO refactor features + let widget = if is_source_request { + page.content.to_text_source(&data) + } else { + page.content.to_text_gemini(&base, &data) + };*/ - // Connect `TextView` widget, update `search` model - page.search.set(Some(widget.text_view)); + let widget = page.content.to_text_gemini(&base, &data); - // Update page meta - page.status.replace(Status::Success { time: now() }); - page.title.replace(match widget.meta.title { - Some(title) => title.into(), // @TODO - None => uri_to_title(&base), - }); + // Connect `TextView` widget, update `search` model + page.search.set(Some(widget.text_view)); - // Update window components - page.window_action.find.simple_action.set_enabled(true); - page.browser_action.update.activate(Some(&page.id)); - } + // Update page meta + page.status.replace(Status::Success { time: now() }); + page.title.replace(match widget.meta.title { + Some(title) => title.into(), // @TODO + None => uri_to_title(&base), + }); + + // Update window components + page.window_action.find.simple_action.set_enabled(true); + page.browser_action.update.activate(Some(&page.id)); + } + Text::Plain { data } => todo!(), + }, Response::Download { base, cancellable, diff --git a/src/app/browser/window/tab/item/page/client.rs b/src/app/browser/window/tab/item/page/client.rs index 97383021..670872a2 100644 --- a/src/app/browser/window/tab/item/page/client.rs +++ b/src/app/browser/window/tab/item/page/client.rs @@ -79,7 +79,7 @@ impl Client { let cancellable = self.new_cancellable(); - match Request::parse(query, None) { + match Request::parse(query) { Ok(request) => request.handle(self, cancellable, callback), Err(e) => match e { // return failure response on unsupported scheme detected @@ -90,7 +90,12 @@ impl Client { _ => Request::lookup(query, Some(&cancellable), |result| { callback(match result { // redirection with scheme auto-complete or default search provider - Ok(request) => Response::Redirect(Redirect::Foreground(request)), + Ok(request) => match request { + Request::Gemini(this, _) => { + Response::Redirect(Redirect::Foreground(this.uri)) + } + _ => todo!(), + }, // unresolvable request. Err(e) => Response::Failure(Failure::Error { message: e.to_string(), diff --git a/src/app/browser/window/tab/item/page/client/request.rs b/src/app/browser/window/tab/item/page/client/request.rs index dd52cf74..334fad2b 100644 --- a/src/app/browser/window/tab/item/page/client/request.rs +++ b/src/app/browser/window/tab/item/page/client/request.rs @@ -3,6 +3,8 @@ mod feature; mod gemini; mod search; +use gemini::Gemini; + use super::{Client, Response}; pub use error::Error; use feature::Feature; @@ -13,46 +15,37 @@ use gtk::{ /// Single `Request` API for multiple `Client` drivers pub enum Request { - Gemini { - feature: Feature, - referrer: Option>, - uri: Uri, - }, + Gemini(Gemini, Feature), Titan { referrer: Option>, uri: Uri, - }, + }, // @TODO deprecated } impl Request { // Constructors /// Create new `Self` from featured string - pub fn parse(query: &str, referrer: Option) -> Result { + pub fn parse(query: &str) -> Result { let (feature, request) = Feature::parse(query); match Uri::parse(request, UriFlags::NONE) { - Ok(uri) => Self::from_uri(uri, Some(feature), referrer), + Ok(uri) => Self::from_uri(uri, feature), Err(e) => Err(Error::Glib(e)), } } /// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html) - pub fn from_uri( - uri: Uri, - feature: Option, - referrer: Option, - ) -> Result { + pub fn from_uri(uri: Uri, feature: Feature) -> Result { match uri.scheme().as_str() { - "gemini" => Ok(Self::Gemini { - feature: feature.unwrap_or_default(), - referrer: referrer.map(Box::new), - uri, - }), // @TODO validate request len by constructor - "titan" => Ok(Self::Titan { - referrer: referrer.map(Box::new), - uri, - }), + "gemini" => Ok(Self::Gemini( + Gemini { + uri, + referrer: None, + }, + feature, + )), + "titan" => todo!(), _ => Err(Error::Unsupported), } } @@ -63,7 +56,7 @@ impl Request { // * make search provider optional // * validate request len by gemini specifications pub fn search(query: &str) -> Self { - Self::from_uri(search::tgls(query), None, None).unwrap() // no handler as unexpected + Self::from_uri(search::tgls(query), Feature::Default).unwrap() // no handler as unexpected } /// Create new `Self` using DNS async resolver (slow method) @@ -85,7 +78,7 @@ impl Request { let query = query.trim(); match Uri::parse(query, UriFlags::NONE) { - Ok(uri) => callback(Self::from_uri(uri, None, None)), + Ok(uri) => callback(Self::from_uri(uri, Feature::Default)), Err(_) => { // try default scheme suggestion let suggestion = format!("{DEFAULT_SCHEME}://{query}"); @@ -99,7 +92,7 @@ impl Request { cancellable, move |resolve| { callback(if resolve.is_ok() { - Self::parse(&suggestion, None) + Self::parse(&suggestion) } else { Ok(Self::search(&suggestion)) }) @@ -120,54 +113,9 @@ impl Request { cancellable: Cancellable, callback: impl FnOnce(Response) + 'static, ) { - match &self { - Self::Gemini { .. } => gemini::request(client, self, cancellable, callback), + match self { + Self::Gemini(this, feature) => this.handle(client, cancellable, callback), Self::Titan { .. } => todo!(), } } - - // Getters - - /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) - pub fn as_uri(&self) -> &Uri { - match self { - Self::Gemini { - feature: _, - referrer: _, - uri, - } - | Self::Titan { referrer: _, uri } => uri, - } - } - - /// Get `Feature` reference for `Self` - pub fn feature(&self) -> &Feature { - match self { - Request::Gemini { feature, .. } => feature, - Request::Titan { .. } => &Feature::Default, - } - } - - /// Recursively count referrers of `Self` - /// * useful to apply redirection rules by protocol driver selected - pub fn referrers(&self) -> usize { - match self { - Request::Gemini { referrer, .. } => referrer, - Request::Titan { referrer, .. } => referrer, - } - .as_ref() - .map_or(0, |request| request.referrers()) - + 1 - } -} - -#[test] -fn test_referrers() { - const QUERY: &str = "gemini://geminiprotocol.net"; - - let r1 = Request::parse(QUERY, None).unwrap(); - let r2 = Request::parse(QUERY, Some(r1)).unwrap(); - let r3 = Request::parse(QUERY, Some(r2)).unwrap(); - - assert_eq!(r3.referrers(), 3); } diff --git a/src/app/browser/window/tab/item/page/client/request/gemini.rs b/src/app/browser/window/tab/item/page/client/request/gemini.rs index 4fbe3740..c489ec06 100644 --- a/src/app/browser/window/tab/item/page/client/request/gemini.rs +++ b/src/app/browser/window/tab/item/page/client/request/gemini.rs @@ -1,196 +1,212 @@ -use super::{super::response::*, Client, Feature, Request, Response}; +use super::{super::response::*, Client, Feature, Response}; use gtk::{ gio::Cancellable, glib::{Priority, Uri, UriFlags}, }; -pub fn request( - client: &Client, - request: Request, - cancellable: Cancellable, - callback: impl FnOnce(Response) + 'static, -) { - send( - client, - request.as_uri().clone(), - cancellable.clone(), - move |result| match result { - Ok(response) => handle(request, response, cancellable, callback), - Err(e) => callback(Response::Failure(Failure::Error { - message: e.to_string(), - })), - }, - ) +pub struct Gemini { + pub referrer: Option>, + pub uri: Uri, } -/// Shared request interface for Gemini protocol -fn send( - client: &Client, - uri: Uri, - cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, -) { - let request = uri.to_string(); - client.gemini.request_async( - ggemini::client::Request::gemini(uri.clone()), - Priority::DEFAULT, - cancellable.clone(), - // Search for user certificate match request - // * @TODO this feature does not support multi-protocol yet - match client.profile.identity.gemini.match_scope(&request) { - Some(identity) => match identity.to_tls_certificate() { - Ok(certificate) => Some(certificate), - Err(_) => todo!(), - }, - None => None, - }, - callback, - ) -} +impl Gemini { + // Actions -/// Shared handler for Gemini `Result` -/// * same implementation for Gemini and Titan protocols response -fn handle( - request: Request, - response: ggemini::client::connection::Response, - cancellable: Cancellable, - callback: impl FnOnce(Response) + 'static, -) { - use ggemini::client::connection::response::{data::Text, meta::Status}; - match response.meta.status { - // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected - Status::Input => callback(Response::Input(Input::Response { - base: request.as_uri().clone(), - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Input expected".into(), + pub fn handle( + self, + client: &Client, + cancellable: Cancellable, + callback: impl FnOnce(Response) + 'static, + ) { + use ggemini::client::connection::response::{data::Text, meta::Status}; + + client.gemini.request_async( + ggemini::client::Request::gemini(self.uri.clone()), + Priority::DEFAULT, + cancellable.clone(), + // Search for user certificate match request + // * @TODO this feature does not support multi-protocol yet + match client + .profile + .identity + .gemini + .match_scope(&self.uri.to_string()) + { + Some(identity) => match identity.to_tls_certificate() { + Ok(certificate) => Some(certificate), + Err(_) => panic!(), // unexpected + }, + None => None, }, - })), - Status::SensitiveInput => callback(Response::Input(Input::Sensitive { - base: request.as_uri().clone(), - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Input expected".into(), - }, - })), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 - Status::Success => match response.meta.mime { - Some(mime) => match mime.as_str() { - "text/gemini" => Text::from_stream_async( - response.connection.stream(), - Priority::DEFAULT, - cancellable.clone(), - move |result| match result { - Ok(text) => callback(Response::TextGemini { - base: request.as_uri().clone(), - source: text.to_string(), - is_source_request: matches!(request.feature(), Feature::Source), // @TODO return `Feature`? - }), - Err(e) => callback(Response::Failure(Failure::Mime { - base: request.as_uri().clone(), - mime: mime.to_string(), - message: e.to_string(), + |result| match result { + Ok(response) => { + match response.meta.status { + // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected + Status::Input => callback(Response::Input(Input::Response { + base: self.uri.clone(), + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Input expected".into(), + }, })), - }, - ), - "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { - callback(Response::Stream { - base: request.as_uri().clone(), - mime: mime.to_string(), - stream: response.connection.stream(), - cancellable, - }) + Status::SensitiveInput => callback(Response::Input(Input::Sensitive { + base: self.uri.clone(), + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Input expected".into(), + }, + })), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-20 + Status::Success => match response.meta.mime { + Some(mime) => match mime.as_str() { + "text/gemini" => Text::from_stream_async( + response.connection.stream(), + Priority::DEFAULT, + cancellable.clone(), + move |result| match result { + Ok(text) => callback(Response::Text( + super::super::response::Text::Gemini { + base: self.uri.clone(), + data: text.to_string(), + }, + )), + Err(e) => callback(Response::Failure(Failure::Mime { + base: self.uri.clone(), + mime: mime.to_string(), + message: e.to_string(), + })), + }, + ), + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + callback(Response::Stream { + base: self.uri.clone(), + mime: mime.to_string(), + stream: response.connection.stream(), + cancellable, + }) + } + mime => callback(Response::Failure(Failure::Mime { + base: self.uri.clone(), + mime: mime.to_string(), + message: format!("Content type `{mime}` yet not supported"), + })), + }, + None => callback(Response::Failure(Failure::Error { + message: "MIME type not found".to_string(), + })), + }, + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + Status::Redirect => callback(self.redirect(response, false)), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Status::PermanentRedirect => callback(self.redirect(response, true)), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + Status::CertificateRequest => { + callback(Response::Certificate(Certificate::Request { + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Client certificate required".into(), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + Status::CertificateUnauthorized => { + callback(Response::Certificate(Certificate::Request { + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Certificate not authorized".into(), + }, + })) + } + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + Status::CertificateInvalid => { + callback(Response::Certificate(Certificate::Request { + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Certificate not valid".into(), + }, + })) + } + status => callback(Response::Failure(Failure::Status { + message: format!("Undefined status code `{status}`"), + })), + } } - mime => callback(Response::Failure(Failure::Mime { - base: request.as_uri().clone(), - mime: mime.to_string(), - message: format!("Content type `{mime}` yet not supported"), + Err(e) => callback(Response::Failure(Failure::Error { + message: e.to_string(), })), }, - None => callback(Response::Failure(Failure::Error { - message: "MIME type not found".to_string(), - })), - }, - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Status::Redirect => callback(redirect(request, response, false)), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Status::PermanentRedirect => callback(redirect(request, response, true)), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Status::CertificateRequest => callback(Response::Certificate(Certificate::Request { - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Client certificate required".into(), - }, - })), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - Status::CertificateUnauthorized => callback(Response::Certificate(Certificate::Request { - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Certificate not authorized".into(), - }, - })), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - Status::CertificateInvalid => callback(Response::Certificate(Certificate::Request { - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Certificate not valid".into(), - }, - })), - status => callback(Response::Failure(Failure::Status { - message: format!("Undefined status code `{status}`"), - })), - } -} - -/// `Response::Redirect` builder -/// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) -fn redirect( - request: Request, - response: ggemini::client::connection::Response, - is_permanent: bool, -) -> Response { - // Validate redirection count - if request.referrers() > 5 { - return Response::Failure(Failure::Error { - message: "Max redirection count reached".to_string(), - }); + ) } - // Target URL expected from response meta data - match response.meta.data { - Some(target) => { - match Uri::parse_relative(request.as_uri(), target.as_str(), UriFlags::NONE) { - Ok(target) => { - // Disallow external redirection - if request.as_uri().scheme() != target.scheme() - || request.as_uri().port() != target.port() - || request.as_uri().host() != target.host() - { - return Response::Failure(Failure::Error { - message: "External redirects not allowed by protocol specification" - .to_string(), - }); // @TODO placeholder page with optional link open button - } - // Build new request - match Request::from_uri(target, None, Some(request)) { - Ok(request) => Response::Redirect(if is_permanent { - Redirect::Foreground(request) - } else { - Redirect::Background(request) - }), - Err(e) => Response::Failure(Failure::Error { - message: e.to_string(), - }), - } - } - Err(e) => Response::Failure(Failure::Error { - message: e.to_string(), - }), - } + /// Redirection builder for `Self` + /// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) + fn redirect( + self, + response: ggemini::client::connection::Response, + is_permanent: bool, + ) -> Response { + // Validate redirection count + if self.referrers() > 5 { + return Response::Failure(Failure::Error { + message: "Max redirection count reached".to_string(), + }); } - None => Response::Failure(Failure::Error { - message: "Target address not found".to_string(), - }), + + // Target URL expected from response meta data + match response.meta.data { + Some(target) => { + match Uri::parse_relative(&self.uri, target.as_str(), UriFlags::NONE) { + Ok(target) => { + // Disallow external redirection + if self.uri.scheme() != target.scheme() + || self.uri.port() != target.port() + || self.uri.host() != target.host() + { + return Response::Failure(Failure::Error { + message: "External redirects not allowed by protocol specification" + .to_string(), + }); // @TODO placeholder page with optional link open button + } + // Build new request + Response::Redirect(if is_permanent { + Redirect::Foreground(target) + } else { + Redirect::Background(target) + }) + } + Err(e) => Response::Failure(Failure::Error { + message: e.to_string(), + }), + } + } + None => Response::Failure(Failure::Error { + message: "Target address not found".to_string(), + }), + } + } + + /// Recursively count referrers of `Self` + /// * useful to apply redirection rules by protocol driver selected + pub fn referrers(&self) -> usize { + self.referrer + .as_ref() + .map_or(0, |request| request.referrers()) + + 1 } } + +/* @TODO + +#[test] +fn test_referrers() { + const QUERY: &str = "gemini://geminiprotocol.net"; + + let r1 = Request::parse(QUERY, None).unwrap(); + let r2 = Request::parse(QUERY, Some(r1)).unwrap(); + let r3 = Request::parse(QUERY, Some(r2)).unwrap(); + + assert_eq!(r3.referrers(), 3); +} + +*/ diff --git a/src/app/browser/window/tab/item/page/client/response.rs b/src/app/browser/window/tab/item/page/client/response.rs index 38664431..87677fb2 100644 --- a/src/app/browser/window/tab/item/page/client/response.rs +++ b/src/app/browser/window/tab/item/page/client/response.rs @@ -2,12 +2,14 @@ pub mod certificate; pub mod failure; pub mod input; pub mod redirect; +pub mod text; // Local dependencies pub use certificate::Certificate; pub use failure::Failure; pub use input::Input; pub use redirect::Redirect; +pub use text::Text; // Global dependencies use gtk::{ @@ -24,11 +26,6 @@ pub enum Response { cancellable: Cancellable, }, Failure(Failure), - TextGemini { - base: Uri, - source: String, - is_source_request: bool, - }, Input(Input), Redirect(Redirect), Stream { @@ -37,4 +34,5 @@ pub enum Response { stream: IOStream, cancellable: Cancellable, }, + Text(Text), } diff --git a/src/app/browser/window/tab/item/page/client/response/redirect.rs b/src/app/browser/window/tab/item/page/client/response/redirect.rs index c31c40fd..01dac73c 100644 --- a/src/app/browser/window/tab/item/page/client/response/redirect.rs +++ b/src/app/browser/window/tab/item/page/client/response/redirect.rs @@ -1,6 +1,6 @@ -use super::super::Request; +use gtk::glib::Uri; pub enum Redirect { - Foreground(Request), - Background(Request), + Foreground(Uri), + Background(Uri), } diff --git a/src/app/browser/window/tab/item/page/client/response/text.rs b/src/app/browser/window/tab/item/page/client/response/text.rs new file mode 100644 index 00000000..08ae39f5 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/response/text.rs @@ -0,0 +1,6 @@ +use gtk::glib::Uri; + +pub enum Text { + Gemini { base: Uri, data: String }, + Plain { data: String }, +}