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::{
|
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 {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 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 } => {
|
||||||
|
@ -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 } => {
|
@ -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 { .. }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user