mirror of
https://github.com/YGGverse/Yoda.git
synced 2025-03-13 06:01:21 +00:00
reorganize request routing
This commit is contained in:
parent
d87057a544
commit
b6e1ae4e6a
@ -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 {
|
||||
|
@ -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<Cancellable>,
|
||||
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 {
|
||||
@ -29,12 +35,32 @@ impl Client {
|
||||
|
||||
/// Create new `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 {
|
||||
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
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}),
|
||||
}
|
||||
}
|
@ -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<Request>,
|
||||
/// Single `Request` API for multiple `Client` drivers
|
||||
pub enum Request {
|
||||
Gemini {
|
||||
feature: Feature,
|
||||
referrer: Vec<Self>,
|
||||
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<Vec<Request>>,
|
||||
referrer: Option<Vec<Self>>,
|
||||
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<Request> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
216
src/app/browser/window/tab/item/page/client/request/gemini.rs
Normal file
216
src/app/browser/window/tab/item/page/client/request/gemini.rs
Normal 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(),
|
||||
}),
|
||||
}
|
||||
}
|
@ -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 } => {
|
||||
|
@ -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 } => {
|
@ -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 { .. }
|
||||
|
Loading…
x
Reference in New Issue
Block a user