begin request reorganization with isolated driver imp

This commit is contained in:
yggverse 2025-01-20 07:12:38 +02:00
parent 3eca182ddf
commit df8dea9534
7 changed files with 261 additions and 288 deletions

View file

@ -359,7 +359,7 @@ fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) {
/// * may call itself on Titan response /// * may call itself on Titan response
fn handle(page: &Rc<Page>, response: client::Response) { fn handle(page: &Rc<Page>, response: client::Response) {
use client::{ use client::{
response::{Certificate, Failure, Input, Redirect}, response::{text::Text, Certificate, Failure, Input, Redirect},
Response, Response,
}; };
match response { match response {
@ -466,28 +466,26 @@ fn handle(page: &Rc<Page>, response: client::Response) {
page.browser_action.update.activate(Some(&page.id)); page.browser_action.update.activate(Some(&page.id));
} }
Response::Redirect(this) => match this { Response::Redirect(this) => match this {
Redirect::Background(request) => { Redirect::Background(uri) => load(&page, Some(&uri.to_string()), false),
load(&page, Some(&request.as_uri().to_string()), false) Redirect::Foreground(uri) => {
}
Redirect::Foreground(request) => {
page.navigation page.navigation
.request .request
.widget .widget
.entry .entry
.set_text(&request.as_uri().to_string()); .set_text(&uri.to_string());
load(&page, Some(&request.as_uri().to_string()), false); load(&page, Some(&uri.to_string()), false);
} }
}, },
Response::TextGemini { Response::Text(this) => match this {
base, Text::Gemini { base, data } => {
source, /* @TODO refactor features
is_source_request,
} => {
let widget = if is_source_request { let widget = if is_source_request {
page.content.to_text_source(&source) page.content.to_text_source(&data)
} else { } else {
page.content.to_text_gemini(&base, &source) page.content.to_text_gemini(&base, &data)
}; };*/
let widget = page.content.to_text_gemini(&base, &data);
// Connect `TextView` widget, update `search` model // Connect `TextView` widget, update `search` model
page.search.set(Some(widget.text_view)); page.search.set(Some(widget.text_view));
@ -503,6 +501,8 @@ fn handle(page: &Rc<Page>, response: client::Response) {
page.window_action.find.simple_action.set_enabled(true); page.window_action.find.simple_action.set_enabled(true);
page.browser_action.update.activate(Some(&page.id)); page.browser_action.update.activate(Some(&page.id));
} }
Text::Plain { data } => todo!(),
},
Response::Download { Response::Download {
base, base,
cancellable, cancellable,

View file

@ -79,7 +79,7 @@ impl Client {
let cancellable = self.new_cancellable(); let cancellable = self.new_cancellable();
match Request::parse(query, None) { match Request::parse(query) {
Ok(request) => request.handle(self, cancellable, callback), Ok(request) => request.handle(self, cancellable, callback),
Err(e) => match e { Err(e) => match e {
// return failure response on unsupported scheme detected // return failure response on unsupported scheme detected
@ -90,7 +90,12 @@ impl Client {
_ => Request::lookup(query, Some(&cancellable), |result| { _ => Request::lookup(query, Some(&cancellable), |result| {
callback(match result { callback(match result {
// redirection with scheme auto-complete or default search provider // 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. // unresolvable request.
Err(e) => Response::Failure(Failure::Error { Err(e) => Response::Failure(Failure::Error {
message: e.to_string(), message: e.to_string(),

View file

@ -3,6 +3,8 @@ mod feature;
mod gemini; mod gemini;
mod search; mod search;
use gemini::Gemini;
use super::{Client, Response}; use super::{Client, Response};
pub use error::Error; pub use error::Error;
use feature::Feature; use feature::Feature;
@ -13,46 +15,37 @@ use gtk::{
/// Single `Request` API for multiple `Client` drivers /// Single `Request` API for multiple `Client` drivers
pub enum Request { pub enum Request {
Gemini { Gemini(Gemini, Feature),
feature: Feature,
referrer: Option<Box<Self>>,
uri: Uri,
},
Titan { Titan {
referrer: Option<Box<Self>>, referrer: Option<Box<Self>>,
uri: Uri, uri: Uri,
}, }, // @TODO deprecated
} }
impl Request { impl Request {
// Constructors // Constructors
/// Create new `Self` from featured string /// Create new `Self` from featured string
pub fn parse(query: &str, referrer: Option<Self>) -> Result<Self, Error> { pub fn parse(query: &str) -> Result<Self, Error> {
let (feature, request) = Feature::parse(query); let (feature, request) = Feature::parse(query);
match Uri::parse(request, UriFlags::NONE) { 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)), Err(e) => Err(Error::Glib(e)),
} }
} }
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html) /// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
pub fn from_uri( pub fn from_uri(uri: Uri, feature: Feature) -> Result<Self, Error> {
uri: Uri,
feature: Option<Feature>,
referrer: Option<Self>,
) -> Result<Self, Error> {
match uri.scheme().as_str() { match uri.scheme().as_str() {
"gemini" => Ok(Self::Gemini { "gemini" => Ok(Self::Gemini(
feature: feature.unwrap_or_default(), Gemini {
referrer: referrer.map(Box::new),
uri, uri,
}), // @TODO validate request len by constructor referrer: None,
"titan" => Ok(Self::Titan { },
referrer: referrer.map(Box::new), feature,
uri, )),
}), "titan" => todo!(),
_ => Err(Error::Unsupported), _ => Err(Error::Unsupported),
} }
} }
@ -63,7 +56,7 @@ impl Request {
// * make search provider optional // * make search provider optional
// * validate request len by gemini specifications // * validate request len by gemini specifications
pub fn search(query: &str) -> Self { 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) /// Create new `Self` using DNS async resolver (slow method)
@ -85,7 +78,7 @@ impl Request {
let query = query.trim(); let query = query.trim();
match Uri::parse(query, UriFlags::NONE) { 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(_) => { Err(_) => {
// try default scheme suggestion // try default scheme suggestion
let suggestion = format!("{DEFAULT_SCHEME}://{query}"); let suggestion = format!("{DEFAULT_SCHEME}://{query}");
@ -99,7 +92,7 @@ impl Request {
cancellable, cancellable,
move |resolve| { move |resolve| {
callback(if resolve.is_ok() { callback(if resolve.is_ok() {
Self::parse(&suggestion, None) Self::parse(&suggestion)
} else { } else {
Ok(Self::search(&suggestion)) Ok(Self::search(&suggestion))
}) })
@ -120,54 +113,9 @@ impl Request {
cancellable: Cancellable, cancellable: Cancellable,
callback: impl FnOnce(Response) + 'static, callback: impl FnOnce(Response) + 'static,
) { ) {
match &self { match self {
Self::Gemini { .. } => gemini::request(client, self, cancellable, callback), Self::Gemini(this, feature) => this.handle(client, cancellable, callback),
Self::Titan { .. } => todo!(), 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);
} }

View file

@ -1,74 +1,57 @@
use super::{super::response::*, Client, Feature, Request, Response}; use super::{super::response::*, Client, Feature, Response};
use gtk::{ use gtk::{
gio::Cancellable, gio::Cancellable,
glib::{Priority, Uri, UriFlags}, glib::{Priority, Uri, UriFlags},
}; };
pub fn request( pub struct Gemini {
client: &Client, pub referrer: Option<Box<Self>>,
request: Request, pub uri: Uri,
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(),
})),
},
)
} }
/// Shared request interface for Gemini protocol impl Gemini {
fn send( // Actions
pub fn handle(
self,
client: &Client, client: &Client,
uri: Uri,
cancellable: Cancellable, cancellable: Cancellable,
callback: impl FnOnce(Result<ggemini::client::Response, ggemini::client::Error>) + 'static, callback: impl FnOnce(Response) + 'static,
) { ) {
let request = uri.to_string(); use ggemini::client::connection::response::{data::Text, meta::Status};
client.gemini.request_async( client.gemini.request_async(
ggemini::client::Request::gemini(uri.clone()), ggemini::client::Request::gemini(self.uri.clone()),
Priority::DEFAULT, Priority::DEFAULT,
cancellable.clone(), cancellable.clone(),
// Search for user certificate match request // Search for user certificate match request
// * @TODO this feature does not support multi-protocol yet // * @TODO this feature does not support multi-protocol yet
match client.profile.identity.gemini.match_scope(&request) { match client
.profile
.identity
.gemini
.match_scope(&self.uri.to_string())
{
Some(identity) => match identity.to_tls_certificate() { Some(identity) => match identity.to_tls_certificate() {
Ok(certificate) => Some(certificate), Ok(certificate) => Some(certificate),
Err(_) => todo!(), Err(_) => panic!(), // unexpected
}, },
None => None, None => None,
}, },
callback, |result| match result {
) Ok(response) => {
}
/// 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 { match response.meta.status {
// https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected // https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected
Status::Input => callback(Response::Input(Input::Response { Status::Input => callback(Response::Input(Input::Response {
base: request.as_uri().clone(), base: self.uri.clone(),
title: match response.meta.data { title: match response.meta.data {
Some(data) => data.to_gstring(), Some(data) => data.to_gstring(),
None => "Input expected".into(), None => "Input expected".into(),
}, },
})), })),
Status::SensitiveInput => callback(Response::Input(Input::Sensitive { Status::SensitiveInput => callback(Response::Input(Input::Sensitive {
base: request.as_uri().clone(), base: self.uri.clone(),
title: match response.meta.data { title: match response.meta.data {
Some(data) => data.to_gstring(), Some(data) => data.to_gstring(),
None => "Input expected".into(), None => "Input expected".into(),
@ -82,13 +65,14 @@ fn handle(
Priority::DEFAULT, Priority::DEFAULT,
cancellable.clone(), cancellable.clone(),
move |result| match result { move |result| match result {
Ok(text) => callback(Response::TextGemini { Ok(text) => callback(Response::Text(
base: request.as_uri().clone(), super::super::response::Text::Gemini {
source: text.to_string(), base: self.uri.clone(),
is_source_request: matches!(request.feature(), Feature::Source), // @TODO return `Feature`? data: text.to_string(),
}), },
)),
Err(e) => callback(Response::Failure(Failure::Mime { Err(e) => callback(Response::Failure(Failure::Mime {
base: request.as_uri().clone(), base: self.uri.clone(),
mime: mime.to_string(), mime: mime.to_string(),
message: e.to_string(), message: e.to_string(),
})), })),
@ -96,14 +80,14 @@ fn handle(
), ),
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => { "image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
callback(Response::Stream { callback(Response::Stream {
base: request.as_uri().clone(), base: self.uri.clone(),
mime: mime.to_string(), mime: mime.to_string(),
stream: response.connection.stream(), stream: response.connection.stream(),
cancellable, cancellable,
}) })
} }
mime => callback(Response::Failure(Failure::Mime { mime => callback(Response::Failure(Failure::Mime {
base: request.as_uri().clone(), base: self.uri.clone(),
mime: mime.to_string(), mime: mime.to_string(),
message: format!("Content type `{mime}` yet not supported"), message: format!("Content type `{mime}` yet not supported"),
})), })),
@ -113,45 +97,57 @@ fn handle(
})), })),
}, },
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection // https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
Status::Redirect => callback(redirect(request, response, false)), Status::Redirect => callback(self.redirect(response, false)),
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection // https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
Status::PermanentRedirect => callback(redirect(request, response, true)), Status::PermanentRedirect => callback(self.redirect(response, true)),
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 // https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
Status::CertificateRequest => callback(Response::Certificate(Certificate::Request { Status::CertificateRequest => {
callback(Response::Certificate(Certificate::Request {
title: match response.meta.data { title: match response.meta.data {
Some(data) => data.to_gstring(), Some(data) => data.to_gstring(),
None => "Client certificate required".into(), None => "Client certificate required".into(),
}, },
})), }))
}
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized // https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
Status::CertificateUnauthorized => callback(Response::Certificate(Certificate::Request { Status::CertificateUnauthorized => {
callback(Response::Certificate(Certificate::Request {
title: match response.meta.data { title: match response.meta.data {
Some(data) => data.to_gstring(), Some(data) => data.to_gstring(),
None => "Certificate not authorized".into(), None => "Certificate not authorized".into(),
}, },
})), }))
}
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid // https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
Status::CertificateInvalid => callback(Response::Certificate(Certificate::Request { Status::CertificateInvalid => {
callback(Response::Certificate(Certificate::Request {
title: match response.meta.data { title: match response.meta.data {
Some(data) => data.to_gstring(), Some(data) => data.to_gstring(),
None => "Certificate not valid".into(), None => "Certificate not valid".into(),
}, },
})), }))
}
status => callback(Response::Failure(Failure::Status { status => callback(Response::Failure(Failure::Status {
message: format!("Undefined status code `{status}`"), message: format!("Undefined status code `{status}`"),
})), })),
} }
} }
Err(e) => callback(Response::Failure(Failure::Error {
message: e.to_string(),
})),
},
)
}
/// `Response::Redirect` builder /// Redirection builder for `Self`
/// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) /// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection)
fn redirect( fn redirect(
request: Request, self,
response: ggemini::client::connection::Response, response: ggemini::client::connection::Response,
is_permanent: bool, is_permanent: bool,
) -> Response { ) -> Response {
// Validate redirection count // Validate redirection count
if request.referrers() > 5 { if self.referrers() > 5 {
return Response::Failure(Failure::Error { return Response::Failure(Failure::Error {
message: "Max redirection count reached".to_string(), message: "Max redirection count reached".to_string(),
}); });
@ -160,12 +156,12 @@ fn redirect(
// Target URL expected from response meta data // Target URL expected from response meta data
match response.meta.data { match response.meta.data {
Some(target) => { Some(target) => {
match Uri::parse_relative(request.as_uri(), target.as_str(), UriFlags::NONE) { match Uri::parse_relative(&self.uri, target.as_str(), UriFlags::NONE) {
Ok(target) => { Ok(target) => {
// Disallow external redirection // Disallow external redirection
if request.as_uri().scheme() != target.scheme() if self.uri.scheme() != target.scheme()
|| request.as_uri().port() != target.port() || self.uri.port() != target.port()
|| request.as_uri().host() != target.host() || self.uri.host() != target.host()
{ {
return Response::Failure(Failure::Error { return Response::Failure(Failure::Error {
message: "External redirects not allowed by protocol specification" message: "External redirects not allowed by protocol specification"
@ -173,16 +169,11 @@ fn redirect(
}); // @TODO placeholder page with optional link open button }); // @TODO placeholder page with optional link open button
} }
// Build new request // Build new request
match Request::from_uri(target, None, Some(request)) { Response::Redirect(if is_permanent {
Ok(request) => Response::Redirect(if is_permanent { Redirect::Foreground(target)
Redirect::Foreground(request)
} else { } else {
Redirect::Background(request) Redirect::Background(target)
}), })
Err(e) => Response::Failure(Failure::Error {
message: e.to_string(),
}),
}
} }
Err(e) => Response::Failure(Failure::Error { Err(e) => Response::Failure(Failure::Error {
message: e.to_string(), message: e.to_string(),
@ -193,4 +184,29 @@ fn redirect(
message: "Target address not found".to_string(), 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);
}
*/

View file

@ -2,12 +2,14 @@ pub mod certificate;
pub mod failure; pub mod failure;
pub mod input; pub mod input;
pub mod redirect; pub mod redirect;
pub mod text;
// Local dependencies // Local dependencies
pub use certificate::Certificate; pub use certificate::Certificate;
pub use failure::Failure; pub use failure::Failure;
pub use input::Input; pub use input::Input;
pub use redirect::Redirect; pub use redirect::Redirect;
pub use text::Text;
// Global dependencies // Global dependencies
use gtk::{ use gtk::{
@ -24,11 +26,6 @@ pub enum Response {
cancellable: Cancellable, cancellable: Cancellable,
}, },
Failure(Failure), Failure(Failure),
TextGemini {
base: Uri,
source: String,
is_source_request: bool,
},
Input(Input), Input(Input),
Redirect(Redirect), Redirect(Redirect),
Stream { Stream {
@ -37,4 +34,5 @@ pub enum Response {
stream: IOStream, stream: IOStream,
cancellable: Cancellable, cancellable: Cancellable,
}, },
Text(Text),
} }

View file

@ -1,6 +1,6 @@
use super::super::Request; use gtk::glib::Uri;
pub enum Redirect { pub enum Redirect {
Foreground(Request), Foreground(Uri),
Background(Request), Background(Uri),
} }

View file

@ -0,0 +1,6 @@
use gtk::glib::Uri;
pub enum Text {
Gemini { base: Uri, data: String },
Plain { data: String },
}