mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-02 09:35:28 +00:00
begin request reorganization with isolated driver imp
This commit is contained in:
parent
3eca182ddf
commit
df8dea9534
7 changed files with 261 additions and 288 deletions
|
|
@ -359,7 +359,7 @@ fn snap_history(profile: &Profile, navigation: &Navigation, uri: Option<&Uri>) {
|
||||||
/// * may call itself on Titan response
|
/// * may call itself on Titan response
|
||||||
fn handle(page: &Rc<Page>, response: client::Response) {
|
fn handle(page: &Rc<Page>, response: client::Response) {
|
||||||
use client::{
|
use client::{
|
||||||
response::{Certificate, Failure, Input, Redirect},
|
response::{text::Text, Certificate, Failure, Input, Redirect},
|
||||||
Response,
|
Response,
|
||||||
};
|
};
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -466,43 +466,43 @@ fn handle(page: &Rc<Page>, response: client::Response) {
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
}
|
}
|
||||||
Response::Redirect(this) => match this {
|
Response::Redirect(this) => match this {
|
||||||
Redirect::Background(request) => {
|
Redirect::Background(uri) => load(&page, Some(&uri.to_string()), false),
|
||||||
load(&page, Some(&request.as_uri().to_string()), false)
|
Redirect::Foreground(uri) => {
|
||||||
}
|
|
||||||
Redirect::Foreground(request) => {
|
|
||||||
page.navigation
|
page.navigation
|
||||||
.request
|
.request
|
||||||
.widget
|
.widget
|
||||||
.entry
|
.entry
|
||||||
.set_text(&request.as_uri().to_string());
|
.set_text(&uri.to_string());
|
||||||
load(&page, Some(&request.as_uri().to_string()), false);
|
load(&page, Some(&uri.to_string()), false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Response::TextGemini {
|
Response::Text(this) => match this {
|
||||||
base,
|
Text::Gemini { base, data } => {
|
||||||
source,
|
/* @TODO refactor features
|
||||||
is_source_request,
|
let widget = if is_source_request {
|
||||||
} => {
|
page.content.to_text_source(&data)
|
||||||
let widget = if is_source_request {
|
} else {
|
||||||
page.content.to_text_source(&source)
|
page.content.to_text_gemini(&base, &data)
|
||||||
} else {
|
};*/
|
||||||
page.content.to_text_gemini(&base, &source)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect `TextView` widget, update `search` model
|
let widget = page.content.to_text_gemini(&base, &data);
|
||||||
page.search.set(Some(widget.text_view));
|
|
||||||
|
|
||||||
// Update page meta
|
// Connect `TextView` widget, update `search` model
|
||||||
page.status.replace(Status::Success { time: now() });
|
page.search.set(Some(widget.text_view));
|
||||||
page.title.replace(match widget.meta.title {
|
|
||||||
Some(title) => title.into(), // @TODO
|
|
||||||
None => uri_to_title(&base),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update window components
|
// Update page meta
|
||||||
page.window_action.find.simple_action.set_enabled(true);
|
page.status.replace(Status::Success { time: now() });
|
||||||
page.browser_action.update.activate(Some(&page.id));
|
page.title.replace(match widget.meta.title {
|
||||||
}
|
Some(title) => title.into(), // @TODO
|
||||||
|
None => uri_to_title(&base),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update window components
|
||||||
|
page.window_action.find.simple_action.set_enabled(true);
|
||||||
|
page.browser_action.update.activate(Some(&page.id));
|
||||||
|
}
|
||||||
|
Text::Plain { data } => todo!(),
|
||||||
|
},
|
||||||
Response::Download {
|
Response::Download {
|
||||||
base,
|
base,
|
||||||
cancellable,
|
cancellable,
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ impl Client {
|
||||||
|
|
||||||
let cancellable = self.new_cancellable();
|
let cancellable = self.new_cancellable();
|
||||||
|
|
||||||
match Request::parse(query, None) {
|
match Request::parse(query) {
|
||||||
Ok(request) => request.handle(self, cancellable, callback),
|
Ok(request) => request.handle(self, cancellable, callback),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
// return failure response on unsupported scheme detected
|
// return failure response on unsupported scheme detected
|
||||||
|
|
@ -90,7 +90,12 @@ impl Client {
|
||||||
_ => Request::lookup(query, Some(&cancellable), |result| {
|
_ => Request::lookup(query, Some(&cancellable), |result| {
|
||||||
callback(match result {
|
callback(match result {
|
||||||
// redirection with scheme auto-complete or default search provider
|
// redirection with scheme auto-complete or default search provider
|
||||||
Ok(request) => Response::Redirect(Redirect::Foreground(request)),
|
Ok(request) => match request {
|
||||||
|
Request::Gemini(this, _) => {
|
||||||
|
Response::Redirect(Redirect::Foreground(this.uri))
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
// unresolvable request.
|
// unresolvable request.
|
||||||
Err(e) => Response::Failure(Failure::Error {
|
Err(e) => Response::Failure(Failure::Error {
|
||||||
message: e.to_string(),
|
message: e.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ mod feature;
|
||||||
mod gemini;
|
mod gemini;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
|
use gemini::Gemini;
|
||||||
|
|
||||||
use super::{Client, Response};
|
use super::{Client, Response};
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
use feature::Feature;
|
use feature::Feature;
|
||||||
|
|
@ -13,46 +15,37 @@ use gtk::{
|
||||||
|
|
||||||
/// Single `Request` API for multiple `Client` drivers
|
/// Single `Request` API for multiple `Client` drivers
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
Gemini {
|
Gemini(Gemini, Feature),
|
||||||
feature: Feature,
|
|
||||||
referrer: Option<Box<Self>>,
|
|
||||||
uri: Uri,
|
|
||||||
},
|
|
||||||
Titan {
|
Titan {
|
||||||
referrer: Option<Box<Self>>,
|
referrer: Option<Box<Self>>,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
},
|
}, // @TODO deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/// Create new `Self` from featured string
|
/// Create new `Self` from featured string
|
||||||
pub fn parse(query: &str, referrer: Option<Self>) -> Result<Self, Error> {
|
pub fn parse(query: &str) -> Result<Self, Error> {
|
||||||
let (feature, request) = Feature::parse(query);
|
let (feature, request) = Feature::parse(query);
|
||||||
|
|
||||||
match Uri::parse(request, UriFlags::NONE) {
|
match Uri::parse(request, UriFlags::NONE) {
|
||||||
Ok(uri) => Self::from_uri(uri, Some(feature), referrer),
|
Ok(uri) => Self::from_uri(uri, feature),
|
||||||
Err(e) => Err(Error::Glib(e)),
|
Err(e) => Err(Error::Glib(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
/// Create new `Self` from [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
||||||
pub fn from_uri(
|
pub fn from_uri(uri: Uri, feature: Feature) -> Result<Self, Error> {
|
||||||
uri: Uri,
|
|
||||||
feature: Option<Feature>,
|
|
||||||
referrer: Option<Self>,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
match uri.scheme().as_str() {
|
match uri.scheme().as_str() {
|
||||||
"gemini" => Ok(Self::Gemini {
|
"gemini" => Ok(Self::Gemini(
|
||||||
feature: feature.unwrap_or_default(),
|
Gemini {
|
||||||
referrer: referrer.map(Box::new),
|
uri,
|
||||||
uri,
|
referrer: None,
|
||||||
}), // @TODO validate request len by constructor
|
},
|
||||||
"titan" => Ok(Self::Titan {
|
feature,
|
||||||
referrer: referrer.map(Box::new),
|
)),
|
||||||
uri,
|
"titan" => todo!(),
|
||||||
}),
|
|
||||||
_ => Err(Error::Unsupported),
|
_ => Err(Error::Unsupported),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +56,7 @@ impl Request {
|
||||||
// * make search provider optional
|
// * make search provider optional
|
||||||
// * validate request len by gemini specifications
|
// * validate request len by gemini specifications
|
||||||
pub fn search(query: &str) -> Self {
|
pub fn search(query: &str) -> Self {
|
||||||
Self::from_uri(search::tgls(query), None, None).unwrap() // no handler as unexpected
|
Self::from_uri(search::tgls(query), Feature::Default).unwrap() // no handler as unexpected
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new `Self` using DNS async resolver (slow method)
|
/// Create new `Self` using DNS async resolver (slow method)
|
||||||
|
|
@ -85,7 +78,7 @@ impl Request {
|
||||||
let query = query.trim();
|
let query = query.trim();
|
||||||
|
|
||||||
match Uri::parse(query, UriFlags::NONE) {
|
match Uri::parse(query, UriFlags::NONE) {
|
||||||
Ok(uri) => callback(Self::from_uri(uri, None, None)),
|
Ok(uri) => callback(Self::from_uri(uri, Feature::Default)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// try default scheme suggestion
|
// try default scheme suggestion
|
||||||
let suggestion = format!("{DEFAULT_SCHEME}://{query}");
|
let suggestion = format!("{DEFAULT_SCHEME}://{query}");
|
||||||
|
|
@ -99,7 +92,7 @@ impl Request {
|
||||||
cancellable,
|
cancellable,
|
||||||
move |resolve| {
|
move |resolve| {
|
||||||
callback(if resolve.is_ok() {
|
callback(if resolve.is_ok() {
|
||||||
Self::parse(&suggestion, None)
|
Self::parse(&suggestion)
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::search(&suggestion))
|
Ok(Self::search(&suggestion))
|
||||||
})
|
})
|
||||||
|
|
@ -120,54 +113,9 @@ impl Request {
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
callback: impl FnOnce(Response) + 'static,
|
callback: impl FnOnce(Response) + 'static,
|
||||||
) {
|
) {
|
||||||
match &self {
|
match self {
|
||||||
Self::Gemini { .. } => gemini::request(client, self, cancellable, callback),
|
Self::Gemini(this, feature) => this.handle(client, cancellable, callback),
|
||||||
Self::Titan { .. } => todo!(),
|
Self::Titan { .. } => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
|
||||||
|
|
||||||
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
|
|
||||||
pub fn as_uri(&self) -> &Uri {
|
|
||||||
match self {
|
|
||||||
Self::Gemini {
|
|
||||||
feature: _,
|
|
||||||
referrer: _,
|
|
||||||
uri,
|
|
||||||
}
|
|
||||||
| Self::Titan { referrer: _, uri } => uri,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get `Feature` reference for `Self`
|
|
||||||
pub fn feature(&self) -> &Feature {
|
|
||||||
match self {
|
|
||||||
Request::Gemini { feature, .. } => feature,
|
|
||||||
Request::Titan { .. } => &Feature::Default,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively count referrers of `Self`
|
|
||||||
/// * useful to apply redirection rules by protocol driver selected
|
|
||||||
pub fn referrers(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Request::Gemini { referrer, .. } => referrer,
|
|
||||||
Request::Titan { referrer, .. } => referrer,
|
|
||||||
}
|
|
||||||
.as_ref()
|
|
||||||
.map_or(0, |request| request.referrers())
|
|
||||||
+ 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_referrers() {
|
|
||||||
const QUERY: &str = "gemini://geminiprotocol.net";
|
|
||||||
|
|
||||||
let r1 = Request::parse(QUERY, None).unwrap();
|
|
||||||
let r2 = Request::parse(QUERY, Some(r1)).unwrap();
|
|
||||||
let r3 = Request::parse(QUERY, Some(r2)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(r3.referrers(), 3);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,196 +1,212 @@
|
||||||
use super::{super::response::*, Client, Feature, Request, Response};
|
use super::{super::response::*, Client, Feature, Response};
|
||||||
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gio::Cancellable,
|
gio::Cancellable,
|
||||||
glib::{Priority, Uri, UriFlags},
|
glib::{Priority, Uri, UriFlags},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn request(
|
pub struct Gemini {
|
||||||
client: &Client,
|
pub referrer: Option<Box<Self>>,
|
||||||
request: Request,
|
pub uri: Uri,
|
||||||
cancellable: Cancellable,
|
|
||||||
callback: impl FnOnce(Response) + 'static,
|
|
||||||
) {
|
|
||||||
send(
|
|
||||||
client,
|
|
||||||
request.as_uri().clone(),
|
|
||||||
cancellable.clone(),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(response) => handle(request, response, cancellable, callback),
|
|
||||||
Err(e) => callback(Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared request interface for Gemini protocol
|
impl Gemini {
|
||||||
fn send(
|
// Actions
|
||||||
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`
|
pub fn handle(
|
||||||
/// * same implementation for Gemini and Titan protocols response
|
self,
|
||||||
fn handle(
|
client: &Client,
|
||||||
request: Request,
|
cancellable: Cancellable,
|
||||||
response: ggemini::client::connection::Response,
|
callback: impl FnOnce(Response) + 'static,
|
||||||
cancellable: Cancellable,
|
) {
|
||||||
callback: impl FnOnce(Response) + 'static,
|
use ggemini::client::connection::response::{data::Text, meta::Status};
|
||||||
) {
|
|
||||||
use ggemini::client::connection::response::{data::Text, meta::Status};
|
client.gemini.request_async(
|
||||||
match response.meta.status {
|
ggemini::client::Request::gemini(self.uri.clone()),
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected
|
Priority::DEFAULT,
|
||||||
Status::Input => callback(Response::Input(Input::Response {
|
cancellable.clone(),
|
||||||
base: request.as_uri().clone(),
|
// Search for user certificate match request
|
||||||
title: match response.meta.data {
|
// * @TODO this feature does not support multi-protocol yet
|
||||||
Some(data) => data.to_gstring(),
|
match client
|
||||||
None => "Input expected".into(),
|
.profile
|
||||||
|
.identity
|
||||||
|
.gemini
|
||||||
|
.match_scope(&self.uri.to_string())
|
||||||
|
{
|
||||||
|
Some(identity) => match identity.to_tls_certificate() {
|
||||||
|
Ok(certificate) => Some(certificate),
|
||||||
|
Err(_) => panic!(), // unexpected
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
},
|
},
|
||||||
})),
|
|result| match result {
|
||||||
Status::SensitiveInput => callback(Response::Input(Input::Sensitive {
|
Ok(response) => {
|
||||||
base: request.as_uri().clone(),
|
match response.meta.status {
|
||||||
title: match response.meta.data {
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected
|
||||||
Some(data) => data.to_gstring(),
|
Status::Input => callback(Response::Input(Input::Response {
|
||||||
None => "Input expected".into(),
|
base: self.uri.clone(),
|
||||||
},
|
title: match response.meta.data {
|
||||||
})),
|
Some(data) => data.to_gstring(),
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
None => "Input expected".into(),
|
||||||
Status::Success => match response.meta.mime {
|
},
|
||||||
Some(mime) => match mime.as_str() {
|
|
||||||
"text/gemini" => Text::from_stream_async(
|
|
||||||
response.connection.stream(),
|
|
||||||
Priority::DEFAULT,
|
|
||||||
cancellable.clone(),
|
|
||||||
move |result| match result {
|
|
||||||
Ok(text) => callback(Response::TextGemini {
|
|
||||||
base: request.as_uri().clone(),
|
|
||||||
source: text.to_string(),
|
|
||||||
is_source_request: matches!(request.feature(), Feature::Source), // @TODO return `Feature`?
|
|
||||||
}),
|
|
||||||
Err(e) => callback(Response::Failure(Failure::Mime {
|
|
||||||
base: request.as_uri().clone(),
|
|
||||||
mime: mime.to_string(),
|
|
||||||
message: e.to_string(),
|
|
||||||
})),
|
})),
|
||||||
},
|
Status::SensitiveInput => callback(Response::Input(Input::Sensitive {
|
||||||
),
|
base: self.uri.clone(),
|
||||||
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
|
title: match response.meta.data {
|
||||||
callback(Response::Stream {
|
Some(data) => data.to_gstring(),
|
||||||
base: request.as_uri().clone(),
|
None => "Input expected".into(),
|
||||||
mime: mime.to_string(),
|
},
|
||||||
stream: response.connection.stream(),
|
})),
|
||||||
cancellable,
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-20
|
||||||
})
|
Status::Success => match response.meta.mime {
|
||||||
|
Some(mime) => match mime.as_str() {
|
||||||
|
"text/gemini" => Text::from_stream_async(
|
||||||
|
response.connection.stream(),
|
||||||
|
Priority::DEFAULT,
|
||||||
|
cancellable.clone(),
|
||||||
|
move |result| match result {
|
||||||
|
Ok(text) => callback(Response::Text(
|
||||||
|
super::super::response::Text::Gemini {
|
||||||
|
base: self.uri.clone(),
|
||||||
|
data: text.to_string(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Err(e) => callback(Response::Failure(Failure::Mime {
|
||||||
|
base: self.uri.clone(),
|
||||||
|
mime: mime.to_string(),
|
||||||
|
message: e.to_string(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"image/png" | "image/gif" | "image/jpeg" | "image/webp" => {
|
||||||
|
callback(Response::Stream {
|
||||||
|
base: self.uri.clone(),
|
||||||
|
mime: mime.to_string(),
|
||||||
|
stream: response.connection.stream(),
|
||||||
|
cancellable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mime => callback(Response::Failure(Failure::Mime {
|
||||||
|
base: self.uri.clone(),
|
||||||
|
mime: mime.to_string(),
|
||||||
|
message: format!("Content type `{mime}` yet not supported"),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
None => callback(Response::Failure(Failure::Error {
|
||||||
|
message: "MIME type not found".to_string(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection
|
||||||
|
Status::Redirect => callback(self.redirect(response, false)),
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
||||||
|
Status::PermanentRedirect => callback(self.redirect(response, true)),
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
|
||||||
|
Status::CertificateRequest => {
|
||||||
|
callback(Response::Certificate(Certificate::Request {
|
||||||
|
title: match response.meta.data {
|
||||||
|
Some(data) => data.to_gstring(),
|
||||||
|
None => "Client certificate required".into(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
||||||
|
Status::CertificateUnauthorized => {
|
||||||
|
callback(Response::Certificate(Certificate::Request {
|
||||||
|
title: match response.meta.data {
|
||||||
|
Some(data) => data.to_gstring(),
|
||||||
|
None => "Certificate not authorized".into(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
||||||
|
Status::CertificateInvalid => {
|
||||||
|
callback(Response::Certificate(Certificate::Request {
|
||||||
|
title: match response.meta.data {
|
||||||
|
Some(data) => data.to_gstring(),
|
||||||
|
None => "Certificate not valid".into(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
status => callback(Response::Failure(Failure::Status {
|
||||||
|
message: format!("Undefined status code `{status}`"),
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mime => callback(Response::Failure(Failure::Mime {
|
Err(e) => callback(Response::Failure(Failure::Error {
|
||||||
base: request.as_uri().clone(),
|
message: e.to_string(),
|
||||||
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(request, response, false)),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection
|
|
||||||
Status::PermanentRedirect => callback(redirect(request, response, true)),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60
|
|
||||||
Status::CertificateRequest => callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Client certificate required".into(),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized
|
|
||||||
Status::CertificateUnauthorized => callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Certificate not authorized".into(),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid
|
|
||||||
Status::CertificateInvalid => callback(Response::Certificate(Certificate::Request {
|
|
||||||
title: match response.meta.data {
|
|
||||||
Some(data) => data.to_gstring(),
|
|
||||||
None => "Certificate not valid".into(),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
status => callback(Response::Failure(Failure::Status {
|
|
||||||
message: format!("Undefined status code `{status}`"),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `Response::Redirect` builder
|
|
||||||
/// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection)
|
|
||||||
fn redirect(
|
|
||||||
request: Request,
|
|
||||||
response: ggemini::client::connection::Response,
|
|
||||||
is_permanent: bool,
|
|
||||||
) -> Response {
|
|
||||||
// Validate redirection count
|
|
||||||
if request.referrers() > 5 {
|
|
||||||
return Response::Failure(Failure::Error {
|
|
||||||
message: "Max redirection count reached".to_string(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target URL expected from response meta data
|
/// Redirection builder for `Self`
|
||||||
match response.meta.data {
|
/// * [Redirect specification](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection)
|
||||||
Some(target) => {
|
fn redirect(
|
||||||
match Uri::parse_relative(request.as_uri(), target.as_str(), UriFlags::NONE) {
|
self,
|
||||||
Ok(target) => {
|
response: ggemini::client::connection::Response,
|
||||||
// Disallow external redirection
|
is_permanent: bool,
|
||||||
if request.as_uri().scheme() != target.scheme()
|
) -> Response {
|
||||||
|| request.as_uri().port() != target.port()
|
// Validate redirection count
|
||||||
|| request.as_uri().host() != target.host()
|
if self.referrers() > 5 {
|
||||||
{
|
return Response::Failure(Failure::Error {
|
||||||
return Response::Failure(Failure::Error {
|
message: "Max redirection count reached".to_string(),
|
||||||
message: "External redirects not allowed by protocol specification"
|
});
|
||||||
.to_string(),
|
|
||||||
}); // @TODO placeholder page with optional link open button
|
|
||||||
}
|
|
||||||
// Build new request
|
|
||||||
match Request::from_uri(target, None, Some(request)) {
|
|
||||||
Ok(request) => Response::Redirect(if is_permanent {
|
|
||||||
Redirect::Foreground(request)
|
|
||||||
} else {
|
|
||||||
Redirect::Background(request)
|
|
||||||
}),
|
|
||||||
Err(e) => Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Response::Failure(Failure::Error {
|
|
||||||
message: e.to_string(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => Response::Failure(Failure::Error {
|
|
||||||
message: "Target address not found".to_string(),
|
// Target URL expected from response meta data
|
||||||
}),
|
match response.meta.data {
|
||||||
|
Some(target) => {
|
||||||
|
match Uri::parse_relative(&self.uri, target.as_str(), UriFlags::NONE) {
|
||||||
|
Ok(target) => {
|
||||||
|
// Disallow external redirection
|
||||||
|
if self.uri.scheme() != target.scheme()
|
||||||
|
|| self.uri.port() != target.port()
|
||||||
|
|| self.uri.host() != target.host()
|
||||||
|
{
|
||||||
|
return Response::Failure(Failure::Error {
|
||||||
|
message: "External redirects not allowed by protocol specification"
|
||||||
|
.to_string(),
|
||||||
|
}); // @TODO placeholder page with optional link open button
|
||||||
|
}
|
||||||
|
// Build new request
|
||||||
|
Response::Redirect(if is_permanent {
|
||||||
|
Redirect::Foreground(target)
|
||||||
|
} else {
|
||||||
|
Redirect::Background(target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Response::Failure(Failure::Error {
|
||||||
|
message: e.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Response::Failure(Failure::Error {
|
||||||
|
message: "Target address not found".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively count referrers of `Self`
|
||||||
|
/// * useful to apply redirection rules by protocol driver selected
|
||||||
|
pub fn referrers(&self) -> usize {
|
||||||
|
self.referrer
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |request| request.referrers())
|
||||||
|
+ 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* @TODO
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_referrers() {
|
||||||
|
const QUERY: &str = "gemini://geminiprotocol.net";
|
||||||
|
|
||||||
|
let r1 = Request::parse(QUERY, None).unwrap();
|
||||||
|
let r2 = Request::parse(QUERY, Some(r1)).unwrap();
|
||||||
|
let r3 = Request::parse(QUERY, Some(r2)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(r3.referrers(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ pub mod certificate;
|
||||||
pub mod failure;
|
pub mod failure;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod redirect;
|
pub mod redirect;
|
||||||
|
pub mod text;
|
||||||
|
|
||||||
// Local dependencies
|
// Local dependencies
|
||||||
pub use certificate::Certificate;
|
pub use certificate::Certificate;
|
||||||
pub use failure::Failure;
|
pub use failure::Failure;
|
||||||
pub use input::Input;
|
pub use input::Input;
|
||||||
pub use redirect::Redirect;
|
pub use redirect::Redirect;
|
||||||
|
pub use text::Text;
|
||||||
|
|
||||||
// Global dependencies
|
// Global dependencies
|
||||||
use gtk::{
|
use gtk::{
|
||||||
|
|
@ -24,11 +26,6 @@ pub enum Response {
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
},
|
},
|
||||||
Failure(Failure),
|
Failure(Failure),
|
||||||
TextGemini {
|
|
||||||
base: Uri,
|
|
||||||
source: String,
|
|
||||||
is_source_request: bool,
|
|
||||||
},
|
|
||||||
Input(Input),
|
Input(Input),
|
||||||
Redirect(Redirect),
|
Redirect(Redirect),
|
||||||
Stream {
|
Stream {
|
||||||
|
|
@ -37,4 +34,5 @@ pub enum Response {
|
||||||
stream: IOStream,
|
stream: IOStream,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
},
|
},
|
||||||
|
Text(Text),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use super::super::Request;
|
use gtk::glib::Uri;
|
||||||
|
|
||||||
pub enum Redirect {
|
pub enum Redirect {
|
||||||
Foreground(Request),
|
Foreground(Uri),
|
||||||
Background(Request),
|
Background(Uri),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
use gtk::glib::Uri;
|
||||||
|
|
||||||
|
pub enum Text {
|
||||||
|
Gemini { base: Uri, data: String },
|
||||||
|
Plain { data: String },
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue