reorganize request routing

This commit is contained in:
yggverse 2025-01-18 12:53:23 +02:00
parent d87057a544
commit b6e1ae4e6a
11 changed files with 329 additions and 527 deletions

View File

@ -23,7 +23,7 @@ use crate::tool::now;
use gtk::{ use gtk::{
gdk::Texture, gdk::Texture,
gdk_pixbuf::Pixbuf, gdk_pixbuf::Pixbuf,
glib::{gformat, GString, Priority, Uri}, glib::{GString, Priority, Uri},
prelude::{EditableExt, FileExt}, prelude::{EditableExt, FileExt},
}; };
use sqlite::Transaction; use sqlite::Transaction;
@ -307,7 +307,7 @@ impl Page {
.request .request
.widget .widget
.entry .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 } => { Response::TextGemini { base, source, is_source_request } => {
let widget = if is_source_request { let widget = if is_source_request {

View File

@ -1,17 +1,18 @@
pub mod driver;
pub mod request; pub mod request;
pub mod response; pub mod response;
pub mod status; pub mod status;
// Children dependencies // Children dependencies
pub use driver::Driver;
pub use request::Request; pub use request::Request;
pub use response::Response; pub use response::Response;
pub use status::Status; pub use status::Status;
// Global dependencies // Global dependencies
use crate::{tool::now, Profile}; use crate::{tool::now, Profile};
use gtk::{gio::Cancellable, glib::Priority, prelude::CancellableExt}; use gtk::{
gio::{Cancellable, SocketClientEvent},
prelude::{CancellableExt, SocketClientExt},
};
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
rc::Rc, rc::Rc,
@ -21,7 +22,12 @@ use std::{
pub struct Client { pub struct Client {
cancellable: Cell<Cancellable>, cancellable: Cell<Cancellable>,
status: Rc<RefCell<Status>>, status: Rc<RefCell<Status>>,
driver: Driver, /// Profile reference required for Gemini protocol auth (match scope)
profile: Rc<Profile>,
/// Supported clients
/// * gemini driver should be initiated once (on page object init)
/// to process all it connection features properly
gemini: Rc<ggemini::Client>,
} }
impl Client { impl Client {
@ -29,12 +35,32 @@ impl Client {
/// Create new `Self` /// Create new `Self`
pub fn init(profile: &Rc<Profile>, callback: impl Fn(Status) + 'static) -> Self { pub fn init(profile: &Rc<Profile>, 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 { Self {
cancellable: Cell::new(Cancellable::new()), 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" 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 /// Begin new request
/// * the `query` as string, to support system routes (e.g. `source:` prefix) /// * the `query` as string, to support system routes (e.g. `source:` prefix)
pub fn request_async(&self, query: &str, callback: impl FnOnce(Response) + 'static) { pub fn request_async(&self, query: &str, callback: impl FnOnce(Response) + 'static) {
// Update client status
self.status.replace(Status::Request { self.status.replace(Status::Request {
time: now(), time: now(),
value: query.to_string(), value: query.to_string(),
}); });
Request::route(self, query, None, self.new_cancellable(), callback);
self.driver.request_async(
Request::build(query, None, self.new_cancellable(), Priority::DEFAULT),
callback,
);
} }
/// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one /// Get new [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) by cancel previous one

View File

@ -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<Profile>,
/// Supported clients
/// * gemini driver should be initiated once (on page object init)
/// to process all it connection features properly
gemini: Rc<ggemini::Client>,
// other clients here..
}
impl Driver {
// Constructors
/// Init new `Self`
pub fn init(profile: Rc<Profile>, 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
},
}
}
}

View File

@ -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<Profile>,
client: &Rc<ggemini::Client>,
uri: &Uri,
cancellable: &Cancellable,
priority: &Priority,
callback: impl FnOnce(Result<ggemini::client::Response, ggemini::client::Error>) + '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<ggemini::client::connection::Response, ggemini::client::Error>,
base: Uri,
cancellable: Cancellable,
priority: Priority,
referrer: Vec<Request>,
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<ggemini::client::connection::response::meta::Data>,
base: Uri,
referrer: Vec<Request>,
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(),
}),
}
}

View File

@ -1,45 +1,59 @@
pub mod feature; mod feature;
pub use feature::Feature; mod gemini;
use super::{Client, Response};
use feature::Feature;
use gtk::{ use gtk::{
gio::Cancellable, gio::Cancellable,
glib::{Priority, Uri}, glib::{Uri, UriFlags},
}; };
/// Request data wrapper for `Client` /// Single `Request` API for multiple `Client` drivers
#[derive(Clone)] pub enum Request {
pub struct Request { Gemini {
pub feature: Feature, feature: Feature,
/// Requests chain in order to process redirection rules referrer: Vec<Self>,
pub referrer: Vec<Request>, uri: Uri,
},
Titan(Uri),
} }
impl Request { impl Request {
// Constructors // Actions
/// Build new `Self` /// Process request by routed driver
pub fn build( pub fn route(
client: &Client,
query: &str, query: &str,
referrer: Option<Vec<Request>>, referrer: Option<Vec<Self>>,
cancellable: Cancellable, cancellable: Cancellable,
priority: Priority, callback: impl FnOnce(Response) + 'static,
) -> Self { ) {
Self { let (feature, request) = Feature::parse(query);
feature: Feature::build(query, cancellable, priority),
referrer: referrer.unwrap_or_default(), 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 // Getters
/// Copy `Self` to new `referrer` vector /// Get reference to `Self` [URI](https://docs.gtk.org/glib/struct.Uri.html)
pub fn to_referrer(&self) -> Vec<Request> { pub fn as_uri(&self) -> &Uri {
let mut referrer = self.referrer.to_vec(); match self {
referrer.push(self.clone()); Self::Gemini {
referrer feature: _,
} referrer: _,
uri,
pub fn uri(&self) -> Option<&Uri> { }
self.feature.uri() | Self::Titan(uri) => &uri,
}
} }
} }

View File

@ -1,43 +1,40 @@
pub mod protocol; // Feature conversion prefixes
pub use protocol::Protocol; const DOWNLOAD: &str = "download:";
const SOURCE: &str = "source:";
use gtk::{
gio::Cancellable,
glib::{Priority, Uri},
};
/// Feature wrapper for client `Request` /// Feature wrapper for client `Request`
#[derive(Clone)] #[derive(Clone)]
pub enum Feature { pub enum Feature {
Default(Protocol), Default,
Download(Protocol), Download,
Source(Protocol), Source,
// @TODO System(Action) // @TODO System(Action)
} }
impl Feature { impl Feature {
// Constructors // Constructors
/// Parse new `Self` from string /// Parse new `Self` from navigation entry request
pub fn build(query: &str, cancellable: Cancellable, priority: Priority) -> Self { pub fn parse(request: &str) -> (Self, &str) {
if let Some(postfix) = query.strip_prefix("download:") { if let Some(postfix) = request.strip_prefix(DOWNLOAD) {
return Self::Download(Protocol::build(postfix, cancellable, priority)); return (Self::Download, postfix);
} }
if let Some(postfix) = query.strip_prefix("source:") { if let Some(postfix) = request.strip_prefix(SOURCE) {
return Self::Source(Protocol::build(postfix, cancellable, priority)); return (Self::Source, postfix);
} }
Self::Default(Protocol::build(query, cancellable, priority)) (Self::Default, request)
} }
// Getters // Getters
pub fn uri(&self) -> Option<&Uri> { /// Get `Self` as prefix
pub fn as_prefix(&self) -> Option<&str> {
match self { match self {
Self::Default(protocol) | Self::Download(protocol) | Self::Source(protocol) => { Self::Download => Some(DOWNLOAD),
protocol.uri() Self::Source => Some(SOURCE),
} Self::Default => None,
} }
} }
} }

View File

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

View File

@ -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<Vec<Request>>,
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<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,
)
}
/// 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<Vec<Request>>,
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<ggemini::client::connection::response::meta::Data>,
base: Uri,
referrer: Vec<Request>,
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(),
}),
}
}

View File

@ -1,7 +1,9 @@
pub mod failure; pub mod failure;
pub mod gemini;
// Children dependencies // Children dependencies
pub use failure::Failure; pub use failure::Failure;
pub use gemini::Gemini;
// Global dependencies // Global dependencies
use crate::tool::format_time; use crate::tool::format_time;
@ -16,7 +18,7 @@ pub enum Status {
/// Operation cancelled, new `Cancellable` required to continue /// Operation cancelled, new `Cancellable` required to continue
Cancelled { time: DateTime }, Cancelled { time: DateTime },
/// Protocol driver updates /// Protocol driver updates
Driver(super::driver::Status), Gemini(Gemini),
/// Something went wrong /// Something went wrong
Failure { time: DateTime, failure: Failure }, Failure { time: DateTime, failure: Failure },
/// New `request` begin /// New `request` begin
@ -40,7 +42,7 @@ impl Display for Status {
format_time(time) format_time(time)
) )
} }
Self::Driver(status) => { Self::Gemini(status) => {
write!(f, "{status}") write!(f, "{status}")
} }
Self::Failure { time, failure } => { Self::Failure { time, failure } => {

View File

@ -3,8 +3,8 @@ use crate::tool::format_time;
use gtk::glib::DateTime; use gtk::glib::DateTime;
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
/// Shared asset for `Driver` statuses /// Shared asset for `Gemini` statuses
pub enum Status { pub enum Gemini {
Resolving { time: DateTime }, Resolving { time: DateTime },
Resolved { time: DateTime }, Resolved { time: DateTime },
Connecting { time: DateTime }, Connecting { time: DateTime },
@ -16,7 +16,7 @@ pub enum Status {
Complete { time: DateTime }, Complete { time: DateTime },
} }
impl Display for Status { impl Display for Gemini {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
match self { match self {
Self::Resolving { time } => { Self::Resolving { time } => {

View File

@ -1,5 +1,5 @@
/// Global dependencies /// Global dependencies
use super::client::{driver::Status as Driver, Status as Client}; use super::client::{status::Gemini, Status as Client};
use gtk::glib::DateTime; use gtk::glib::DateTime;
/// `Page` status /// `Page` status
@ -28,16 +28,16 @@ impl Status {
| Client::Cancelled { .. } | Client::Cancelled { .. }
| Client::Failure { .. } | Client::Failure { .. }
| Client::Request { .. } => Some(0.0), | Client::Request { .. } => Some(0.0),
Client::Driver(status) => match status { Client::Gemini(status) => match status {
Driver::Resolving { .. } => Some(0.1), Gemini::Resolving { .. } => Some(0.1),
Driver::Resolved { .. } => Some(0.2), Gemini::Resolved { .. } => Some(0.2),
Driver::Connecting { .. } => Some(0.3), Gemini::Connecting { .. } => Some(0.3),
Driver::Connected { .. } => Some(0.4), Gemini::Connected { .. } => Some(0.4),
Driver::ProxyNegotiating { .. } => Some(0.5), Gemini::ProxyNegotiating { .. } => Some(0.5),
Driver::ProxyNegotiated { .. } => Some(0.6), Gemini::ProxyNegotiated { .. } => Some(0.6),
Driver::TlsHandshaking { .. } => Some(0.7), Gemini::TlsHandshaking { .. } => Some(0.7),
Driver::TlsHandshaked { .. } => Some(0.8), Gemini::TlsHandshaked { .. } => Some(0.8),
Driver::Complete { .. } => Some(0.9), Gemini::Complete { .. } => Some(0.9),
}, },
}, },
Self::Failure { .. } Self::Failure { .. }