diff --git a/src/app/browser/window/tab/item/page.rs b/src/app/browser/window/tab/item/page.rs index 158acea0..7f70e654 100644 --- a/src/app/browser/window/tab/item/page.rs +++ b/src/app/browser/window/tab/item/page.rs @@ -23,7 +23,7 @@ use crate::tool::now; use gtk::{ gdk::Texture, gdk_pixbuf::Pixbuf, - glib::{gformat, GString, Priority, Uri}, + glib::{GString, Priority, Uri}, prelude::{EditableExt, FileExt}, }; use sqlite::Transaction; @@ -307,7 +307,7 @@ impl Page { .request .widget .entry - .set_text(&request.uri().unwrap().to_string())} // @TODO handle + .set_text(&request.as_uri().to_string())} // @TODO handle } Response::TextGemini { base, source, is_source_request } => { let widget = if is_source_request { diff --git a/src/app/browser/window/tab/item/page/client.rs b/src/app/browser/window/tab/item/page/client.rs index 769c85aa..a3fe395f 100644 --- a/src/app/browser/window/tab/item/page/client.rs +++ b/src/app/browser/window/tab/item/page/client.rs @@ -1,17 +1,18 @@ -pub mod driver; pub mod request; pub mod response; pub mod status; // Children dependencies -pub use driver::Driver; pub use request::Request; pub use response::Response; pub use status::Status; // Global dependencies use crate::{tool::now, Profile}; -use gtk::{gio::Cancellable, glib::Priority, prelude::CancellableExt}; +use gtk::{ + gio::{Cancellable, SocketClientEvent}, + prelude::{CancellableExt, SocketClientExt}, +}; use std::{ cell::{Cell, RefCell}, rc::Rc, @@ -21,7 +22,12 @@ use std::{ pub struct Client { cancellable: Cell, status: Rc>, - driver: Driver, + /// Profile reference required for Gemini protocol auth (match scope) + profile: Rc, + /// Supported clients + /// * gemini driver should be initiated once (on page object init) + /// to process all it connection features properly + gemini: Rc, } impl Client { @@ -29,12 +35,32 @@ impl Client { /// Create new `Self` pub fn init(profile: &Rc, callback: impl Fn(Status) + 'static) -> Self { + use status::Gemini; + // Init supported protocol libraries + let gemini = Rc::new(ggemini::Client::new()); + + // Retransmit gemini [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html) updates + gemini.socket.connect_event(move |_, event, _, _| { + callback(Status::Gemini(match event { + SocketClientEvent::Resolving => Gemini::Resolving { time: now() }, + SocketClientEvent::Resolved => Gemini::Resolved { time: now() }, + SocketClientEvent::Connecting => Gemini::Connecting { time: now() }, + SocketClientEvent::Connected => Gemini::Connected { time: now() }, + SocketClientEvent::ProxyNegotiating => Gemini::ProxyNegotiating { time: now() }, + SocketClientEvent::ProxyNegotiated => Gemini::ProxyNegotiated { time: now() }, + // * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections! + SocketClientEvent::TlsHandshaking => Gemini::TlsHandshaking { time: now() }, + SocketClientEvent::TlsHandshaked => Gemini::TlsHandshaked { time: now() }, + SocketClientEvent::Complete => Gemini::Complete { time: now() }, + _ => todo!(), // alert on API change + })) + }); + Self { cancellable: Cell::new(Cancellable::new()), - driver: Driver::init(profile.clone(), move |status| { - callback(Status::Driver(status)) - }), status: Rc::new(RefCell::new(Status::Cancellable { time: now() })), // e.g. "ready to use" + profile: profile.clone(), + gemini, } } @@ -43,16 +69,11 @@ impl Client { /// Begin new request /// * the `query` as string, to support system routes (e.g. `source:` prefix) pub fn request_async(&self, query: &str, callback: impl FnOnce(Response) + 'static) { - // Update client status self.status.replace(Status::Request { time: now(), value: query.to_string(), }); - - self.driver.request_async( - Request::build(query, None, self.new_cancellable(), Priority::DEFAULT), - callback, - ); + Request::route(self, query, None, self.new_cancellable(), callback); } /// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one diff --git a/src/app/browser/window/tab/item/page/client/driver.rs b/src/app/browser/window/tab/item/page/client/driver.rs deleted file mode 100644 index 2e2fb695..00000000 --- a/src/app/browser/window/tab/item/page/client/driver.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! At this moment, the `Driver` contain only one protocol library, -//! by extending it features with new protocol, please make sub-module implementation - -mod gemini; -pub mod status; - -// Local dependencies -pub use status::Status; - -// Global dependencies -use super::{ - request::{feature::Protocol, Feature}, - response, - response::Failure, - Request, Response, -}; -use crate::{tool::now, Profile}; -use gtk::{gio::SocketClientEvent, prelude::SocketClientExt}; -use std::rc::Rc; - -pub struct Driver { - /// Profile reference required for Gemini protocol auth (match scope) - profile: Rc, - /// Supported clients - /// * gemini driver should be initiated once (on page object init) - /// to process all it connection features properly - gemini: Rc, - // other clients here.. -} - -impl Driver { - // Constructors - - /// Init new `Self` - pub fn init(profile: Rc, callback: impl Fn(Status) + 'static) -> Self { - // Init supported protocol libraries - let gemini = Rc::new(ggemini::Client::new()); - - // Retransmit gemini [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html) updates - gemini.socket.connect_event(move |_, event, _, _| { - callback(match event { - SocketClientEvent::Resolving => Status::Resolving { time: now() }, - SocketClientEvent::Resolved => Status::Resolved { time: now() }, - SocketClientEvent::Connecting => Status::Connecting { time: now() }, - SocketClientEvent::Connected => Status::Connected { time: now() }, - SocketClientEvent::ProxyNegotiating => Status::ProxyNegotiating { time: now() }, - SocketClientEvent::ProxyNegotiated => Status::ProxyNegotiated { time: now() }, - // * `TlsHandshaking` | `TlsHandshaked` has effect only for guest connections! - SocketClientEvent::TlsHandshaking => Status::TlsHandshaking { time: now() }, - SocketClientEvent::TlsHandshaked => Status::TlsHandshaked { time: now() }, - SocketClientEvent::Complete => Status::Complete { time: now() }, - _ => todo!(), // alert on API change - }) - }); - - // other client listeners here.. - - // Done - Self { profile, gemini } - } - - // Actions - - /// Make new async `Feature` request - /// * return `Response` in callback function - pub fn request_async(&self, request: Request, callback: impl FnOnce(Response) + 'static) { - let referrer = request.to_referrer(); - match request.feature { - Feature::Download(protocol) => match protocol { - Protocol::Gemini { - uri, - cancellable, - priority, - } => gemini::request_async( - &self.profile, - &self.gemini, - &uri, - &cancellable, - &priority, - { - let base = uri.clone(); - let cancellable = cancellable.clone(); - move |result| { - callback(match result { - Ok(response) => Response::Download { - base, - stream: response.connection.stream(), - cancellable, - }, - Err(e) => Response::Failure(Failure::Error { - message: e.to_string(), - }), - }) - } - }, - ), - _ => callback(Response::Failure(Failure::Error { - message: "Download feature yet not supported for this request".to_string(), - })), // @TODO or maybe panic as unexpected - }, - Feature::Default(protocol) => match protocol { - Protocol::Gemini { - uri, - cancellable, - priority, - } => gemini::request_async( - &self.profile, - &self.gemini, - &uri, - &cancellable, - &priority, - { - let cancellable = cancellable.clone(); - let uri = uri.clone(); - - move |result| { - gemini::handle( - result, - uri, - cancellable, - priority, - referrer, - false, - callback, - ) - } - }, - ), - Protocol::Titan { .. } => todo!(), - Protocol::Undefined => todo!(), - }, - Feature::Source(ref protocol) => match protocol { - Protocol::Gemini { - uri, - cancellable, - priority, - } => gemini::request_async( - &self.profile, - &self.gemini, - uri, - cancellable, - priority, - { - let cancellable = cancellable.clone(); - let priority = *priority; - let uri = uri.clone(); - move |result| { - gemini::handle( - result, - uri, - cancellable, - priority, - referrer, - true, - callback, - ) - } - }, - ), - _ => callback(Response::Failure(Failure::Error { - message: "Source view feature yet not supported for this request".to_string(), - })), // @TODO or maybe panic as unexpected - }, - } - } -} diff --git a/src/app/browser/window/tab/item/page/client/driver/gemini.rs b/src/app/browser/window/tab/item/page/client/driver/gemini.rs deleted file mode 100644 index 31ee104f..00000000 --- a/src/app/browser/window/tab/item/page/client/driver/gemini.rs +++ /dev/null @@ -1,205 +0,0 @@ -use super::{ - response::{Certificate, Failure, Input, Redirect}, - Profile, Request, Response, -}; - -use gtk::{ - gio::Cancellable, - glib::{Priority, Uri, UriFlags}, -}; -use std::rc::Rc; - -/// Shared request interface for Gemini protocol -pub fn request_async( - profile: &Rc, - client: &Rc, - uri: &Uri, - cancellable: &Cancellable, - priority: &Priority, - callback: impl FnOnce(Result) + 'static, -) { - let request = uri.to_string(); - client.request_async( - ggemini::client::Request::gemini(uri.clone()), - priority.clone(), - cancellable.clone(), - // Search for user certificate match request - // * @TODO this feature does not support multi-protocol yet - match profile.identity.gemini.match_scope(&request) { - Some(identity) => match identity.to_tls_certificate() { - Ok(certificate) => Some(certificate), - Err(_) => todo!(), - }, - None => None, - }, - callback, - ) -} - -/// Shared handler for Gemini `Result` -/// * same implementation for Gemini and Titan protocols response -pub fn handle( - result: Result, - base: Uri, - cancellable: Cancellable, - priority: Priority, - referrer: Vec, - is_source_request: bool, // @TODO yet partial implementation - callback: impl FnOnce(Response) + 'static, -) { - use ggemini::client::connection::response::{data::Text, meta::Status}; - 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, - title: match response.meta.data { - Some(data) => data.to_gstring(), - None => "Input expected".into(), - }, - })), - Status::SensitiveInput => callback(Response::Input(Input::Sensitive { - base, - 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.clone(), - cancellable.clone(), - move |result| match result { - Ok(text) => callback(Response::TextGemini { - base, - source: text.data, - is_source_request, - }), - Err(_) => todo!(), - }, - ), - "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { - callback(Response::Stream { - base, - mime: mime.to_string(), - stream: response.connection.stream(), - cancellable, - }) - } - mime => callback(Response::Failure(Failure::Mime { - base, - 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(redirect( - response.meta.data, - base, - referrer, - cancellable, - priority, - false, - )), - // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Status::PermanentRedirect => callback(redirect( - response.meta.data, - base, - referrer, - cancellable, - priority, - 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}`"), - })), - }, - Err(e) => callback(Response::Failure(Failure::Error { - message: e.to_string(), - })), - } -} - -/// Shared redirection `Response` builder -fn redirect( - data: Option, - base: Uri, - referrer: Vec, - cancellable: Cancellable, - priority: Priority, - is_foreground: bool, -) -> Response { - // Validate redirection attempt - // [Gemini protocol specifications](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) - if referrer.len() > 5 { - return Response::Failure(Failure::Error { - message: format!("Max redirection count reached"), - }); - } - match data { - // Target address could be relative, parse using base Uri - Some(target) => match Uri::parse_relative(&base, target.as_str(), UriFlags::NONE) { - Ok(target) => { - // Disallow external redirection - if base.scheme() != target.scheme() - || base.port() != target.port() - || base.host() != target.host() - { - return Response::Failure(Failure::Error { - message: format!( - "External redirects not allowed by protocol specification" - ), - }); // @TODO placeholder page with optional link open button - } - - // Build new `Request` for redirection `Response` - // * make sure that `referrer` already contain current `Request` - // (to validate redirection count in chain) - let request = - Request::build(&target.to_string(), Some(referrer), cancellable, priority); - - Response::Redirect(if is_foreground { - Redirect::Foreground(request) - } else { - Redirect::Background(request) - }) - } - Err(e) => Response::Failure(Failure::Error { - message: format!("Could not parse target address: {e}"), - }), - }, - None => Response::Failure(Failure::Error { - message: "Target address not found".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 82cb4e34..759a65ea 100644 --- a/src/app/browser/window/tab/item/page/client/request.rs +++ b/src/app/browser/window/tab/item/page/client/request.rs @@ -1,45 +1,59 @@ -pub mod feature; -pub use feature::Feature; +mod feature; +mod gemini; +use super::{Client, Response}; +use feature::Feature; use gtk::{ gio::Cancellable, - glib::{Priority, Uri}, + glib::{Uri, UriFlags}, }; -/// Request data wrapper for `Client` -#[derive(Clone)] -pub struct Request { - pub feature: Feature, - /// Requests chain in order to process redirection rules - pub referrer: Vec, +/// Single `Request` API for multiple `Client` drivers +pub enum Request { + Gemini { + feature: Feature, + referrer: Vec, + uri: Uri, + }, + Titan(Uri), } impl Request { - // Constructors + // Actions - /// Build new `Self` - pub fn build( + /// Process request by routed driver + pub fn route( + client: &Client, query: &str, - referrer: Option>, + referrer: Option>, cancellable: Cancellable, - priority: Priority, - ) -> Self { - Self { - feature: Feature::build(query, cancellable, priority), - referrer: referrer.unwrap_or_default(), + callback: impl FnOnce(Response) + 'static, + ) { + let (feature, request) = Feature::parse(query); + + match Uri::parse(request, UriFlags::NONE) { + Ok(uri) => match uri.scheme().as_str() { + "gemini" => gemini::route(client, feature, uri, referrer, cancellable, callback), + "titan" => todo!(), + _ => callback(Response::Redirect( + todo!(), //super::response::Redirect::Foreground(()), + )), + }, + Err(_) => todo!(), } } // Getters - /// Copy `Self` to new `referrer` vector - pub fn to_referrer(&self) -> Vec { - let mut referrer = self.referrer.to_vec(); - referrer.push(self.clone()); - referrer - } - - pub fn uri(&self) -> Option<&Uri> { - self.feature.uri() + /// 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(uri) => &uri, + } } } diff --git a/src/app/browser/window/tab/item/page/client/request/feature.rs b/src/app/browser/window/tab/item/page/client/request/feature.rs index 3667f1fc..79f48169 100644 --- a/src/app/browser/window/tab/item/page/client/request/feature.rs +++ b/src/app/browser/window/tab/item/page/client/request/feature.rs @@ -1,43 +1,40 @@ -pub mod protocol; -pub use protocol::Protocol; - -use gtk::{ - gio::Cancellable, - glib::{Priority, Uri}, -}; +// Feature conversion prefixes +const DOWNLOAD: &str = "download:"; +const SOURCE: &str = "source:"; /// Feature wrapper for client `Request` #[derive(Clone)] pub enum Feature { - Default(Protocol), - Download(Protocol), - Source(Protocol), + Default, + Download, + Source, // @TODO System(Action) } impl Feature { // Constructors - /// Parse new `Self` from string - pub fn build(query: &str, cancellable: Cancellable, priority: Priority) -> Self { - if let Some(postfix) = query.strip_prefix("download:") { - return Self::Download(Protocol::build(postfix, cancellable, priority)); + /// Parse new `Self` from navigation entry request + pub fn parse(request: &str) -> (Self, &str) { + if let Some(postfix) = request.strip_prefix(DOWNLOAD) { + return (Self::Download, postfix); } - if let Some(postfix) = query.strip_prefix("source:") { - return Self::Source(Protocol::build(postfix, cancellable, priority)); + if let Some(postfix) = request.strip_prefix(SOURCE) { + return (Self::Source, postfix); } - Self::Default(Protocol::build(query, cancellable, priority)) + (Self::Default, request) } // Getters - pub fn uri(&self) -> Option<&Uri> { + /// Get `Self` as prefix + pub fn as_prefix(&self) -> Option<&str> { match self { - Self::Default(protocol) | Self::Download(protocol) | Self::Source(protocol) => { - protocol.uri() - } + Self::Download => Some(DOWNLOAD), + Self::Source => Some(SOURCE), + Self::Default => None, } } } diff --git a/src/app/browser/window/tab/item/page/client/request/feature/protocol.rs b/src/app/browser/window/tab/item/page/client/request/feature/protocol.rs deleted file mode 100644 index a8128a50..00000000 --- a/src/app/browser/window/tab/item/page/client/request/feature/protocol.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Global dependencies -use gtk::{ - gio::Cancellable, - glib::{Priority, Uri, UriFlags}, -}; - -#[derive(Clone)] -pub enum Protocol { - Gemini { - uri: Uri, - cancellable: Cancellable, - priority: Priority, - }, - Titan { - uri: Uri, - cancellable: Cancellable, - priority: Priority, - }, - Undefined, -} - -impl Protocol { - // Constructors - - /// Create new `Self` from parsable request string - pub fn build(query: &str, cancellable: Cancellable, priority: Priority) -> Self { - match Uri::parse(query, UriFlags::NONE) { - Ok(uri) => match uri.scheme().as_str() { - "gemini" => Self::Gemini { - uri, - cancellable, - priority, - }, - "titan" => Self::Titan { - uri, - cancellable, - priority, - }, - _ => Self::Undefined, - }, - // Search request if the request could not be parsed as the valid [URI](https://docs.gtk.org/glib/struct.Uri.html) - // * @TODO implement DNS lookup before apply this option - Err(_) => Self::Gemini { - uri: Uri::build( - UriFlags::NONE, - "gemini", - None, - Some("tlgs.one"), - -1, - "/search", // beginning slash required to prevent assertion panic on construct - Some(&Uri::escape_string(query, None, false)), // @TODO is `escape_string` really wanted in `build` context? - None, - ), - cancellable, - priority, - }, - } - } - - // Getters - - pub fn uri(&self) -> Option<&Uri> { - match self { - Self::Gemini { - uri, - cancellable: _, - priority: _, - } - | Self::Titan { - uri, - cancellable: _, - priority: _, - } => Some(&uri), - Self::Undefined => None, - } - } -} 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 new file mode 100644 index 00000000..0ff1bf21 --- /dev/null +++ b/src/app/browser/window/tab/item/page/client/request/gemini.rs @@ -0,0 +1,216 @@ +use super::{super::response::*, Client, Feature, Request, Response}; + +use gtk::{ + gio::Cancellable, + glib::{Priority, Uri, UriFlags}, +}; + +pub fn route( + client: &Client, + feature: Feature, + uri: Uri, + referrer: Option>, + cancellable: Cancellable, + callback: impl FnOnce(Response) + 'static, +) { + request( + client, + uri.clone(), + cancellable.clone(), + move |result| match result { + Ok(response) => handle(response, uri, cancellable, referrer, feature, callback), + Err(e) => callback(Response::Failure(Failure::Error { + message: e.to_string(), + })), + }, + ) +} + +/// Shared request interface for Gemini protocol +fn request( + 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, + ) +} + +/// Shared handler for Gemini `Result` +/// * same implementation for Gemini and Titan protocols response +fn handle( + response: ggemini::client::connection::Response, + base: Uri, + cancellable: Cancellable, + referrer: Option>, + feature: Feature, + 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, + title: match response.meta.data { + Some(data) => data.to_gstring(), + None => "Input expected".into(), + }, + })), + Status::SensitiveInput => callback(Response::Input(Input::Sensitive { + base, + 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, + source: text.data, + is_source_request: match feature { + Feature::Source => true, + _ => false, + }, + }), + Err(_) => todo!(), + }, + ), + "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { + callback(Response::Stream { + base, + mime: mime.to_string(), + stream: response.connection.stream(), + cancellable, + }) + } + mime => callback(Response::Failure(Failure::Mime { + base, + 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(redirect( + response.meta.data, + base, + referrer.unwrap_or_default(), // @TODO + cancellable, + Priority::DEFAULT, + false, + )), + // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Status::PermanentRedirect => callback(redirect( + response.meta.data, + base, + referrer.unwrap_or_default(), // @TODO + cancellable, + Priority::DEFAULT, + 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}`"), + })), + } +} + +/// Shared redirection `Response` builder +fn redirect( + data: Option, + base: Uri, + referrer: Vec, + cancellable: Cancellable, + priority: Priority, + is_foreground: bool, +) -> Response { + // Validate redirection attempt + // [Gemini protocol specifications](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) + if referrer.len() > 5 { + return Response::Failure(Failure::Error { + message: format!("Max redirection count reached"), + }); + } + match data { + // Target address could be relative, parse using base Uri + Some(target) => match Uri::parse_relative(&base, target.as_str(), UriFlags::NONE) { + Ok(target) => { + // Disallow external redirection + if base.scheme() != target.scheme() + || base.port() != target.port() + || base.host() != target.host() + { + return Response::Failure(Failure::Error { + message: format!( + "External redirects not allowed by protocol specification" + ), + }); // @TODO placeholder page with optional link open button + } + + // Build new `Request` for redirection `Response` + // * make sure that `referrer` already contain current `Request` + // (to validate redirection count in chain) + todo!() + /*let request = + Request::build(&target.to_string(), Some(referrer), cancellable, priority); + + Response::Redirect(if is_foreground { + Redirect::Foreground(request) + } else { + Redirect::Background(request) + })*/ + } + Err(e) => Response::Failure(Failure::Error { + message: format!("Could not parse target address: {e}"), + }), + }, + None => Response::Failure(Failure::Error { + message: "Target address not found".to_string(), + }), + } +} diff --git a/src/app/browser/window/tab/item/page/client/status.rs b/src/app/browser/window/tab/item/page/client/status.rs index e153a753..60c040e9 100644 --- a/src/app/browser/window/tab/item/page/client/status.rs +++ b/src/app/browser/window/tab/item/page/client/status.rs @@ -1,7 +1,9 @@ pub mod failure; +pub mod gemini; // Children dependencies pub use failure::Failure; +pub use gemini::Gemini; // Global dependencies use crate::tool::format_time; @@ -16,7 +18,7 @@ pub enum Status { /// Operation cancelled, new `Cancellable` required to continue Cancelled { time: DateTime }, /// Protocol driver updates - Driver(super::driver::Status), + Gemini(Gemini), /// Something went wrong Failure { time: DateTime, failure: Failure }, /// New `request` begin @@ -40,7 +42,7 @@ impl Display for Status { format_time(time) ) } - Self::Driver(status) => { + Self::Gemini(status) => { write!(f, "{status}") } Self::Failure { time, failure } => { diff --git a/src/app/browser/window/tab/item/page/client/driver/status.rs b/src/app/browser/window/tab/item/page/client/status/gemini.rs similarity index 95% rename from src/app/browser/window/tab/item/page/client/driver/status.rs rename to src/app/browser/window/tab/item/page/client/status/gemini.rs index 8a9eb692..84854b63 100644 --- a/src/app/browser/window/tab/item/page/client/driver/status.rs +++ b/src/app/browser/window/tab/item/page/client/status/gemini.rs @@ -3,8 +3,8 @@ use crate::tool::format_time; use gtk::glib::DateTime; use std::fmt::{Display, Formatter, Result}; -/// Shared asset for `Driver` statuses -pub enum Status { +/// Shared asset for `Gemini` statuses +pub enum Gemini { Resolving { time: DateTime }, Resolved { time: DateTime }, Connecting { time: DateTime }, @@ -16,7 +16,7 @@ pub enum Status { Complete { time: DateTime }, } -impl Display for Status { +impl Display for Gemini { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::Resolving { time } => { diff --git a/src/app/browser/window/tab/item/page/status.rs b/src/app/browser/window/tab/item/page/status.rs index 4bb7fbe8..3d675fde 100644 --- a/src/app/browser/window/tab/item/page/status.rs +++ b/src/app/browser/window/tab/item/page/status.rs @@ -1,5 +1,5 @@ /// Global dependencies -use super::client::{driver::Status as Driver, Status as Client}; +use super::client::{status::Gemini, Status as Client}; use gtk::glib::DateTime; /// `Page` status @@ -28,16 +28,16 @@ impl Status { | Client::Cancelled { .. } | Client::Failure { .. } | Client::Request { .. } => Some(0.0), - Client::Driver(status) => match status { - Driver::Resolving { .. } => Some(0.1), - Driver::Resolved { .. } => Some(0.2), - Driver::Connecting { .. } => Some(0.3), - Driver::Connected { .. } => Some(0.4), - Driver::ProxyNegotiating { .. } => Some(0.5), - Driver::ProxyNegotiated { .. } => Some(0.6), - Driver::TlsHandshaking { .. } => Some(0.7), - Driver::TlsHandshaked { .. } => Some(0.8), - Driver::Complete { .. } => Some(0.9), + Client::Gemini(status) => match status { + Gemini::Resolving { .. } => Some(0.1), + Gemini::Resolved { .. } => Some(0.2), + Gemini::Connecting { .. } => Some(0.3), + Gemini::Connected { .. } => Some(0.4), + Gemini::ProxyNegotiating { .. } => Some(0.5), + Gemini::ProxyNegotiated { .. } => Some(0.6), + Gemini::TlsHandshaking { .. } => Some(0.7), + Gemini::TlsHandshaked { .. } => Some(0.8), + Gemini::Complete { .. } => Some(0.9), }, }, Self::Failure { .. }