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
fn handle(page: &Rc<Page>, 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<Page>, 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,

View File

@ -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(),

View File

@ -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<Box<Self>>,
uri: Uri,
},
Gemini(Gemini, Feature),
Titan {
referrer: Option<Box<Self>>,
uri: Uri,
},
}, // @TODO deprecated
}
impl Request {
// Constructors
/// 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);
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<Feature>,
referrer: Option<Self>,
) -> Result<Self, Error> {
pub fn from_uri(uri: Uri, feature: Feature) -> Result<Self, Error> {
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);
}

View File

@ -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<Box<Self>>,
pub uri: Uri,
}
/// Shared request interface for Gemini protocol
fn send(
client: &Client,
uri: Uri,
cancellable: Cancellable,
callback: impl FnOnce(Result<ggemini::client::Response, ggemini::client::Error>) + '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);
}
*/

View File

@ -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),
}

View File

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

View File

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