implement Titan protocol features

This commit is contained in:
yggverse 2025-01-13 21:22:03 +02:00
parent 66a0de6a8e
commit 29b835411d
8 changed files with 204 additions and 27 deletions

View file

@ -4,11 +4,11 @@
pub mod connection;
pub mod error;
pub use connection::Connection;
pub use connection::{Connection, Request, Response};
pub use error::Error;
use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate};
use glib::{Priority, Uri};
use glib::Priority;
// Defaults
@ -56,16 +56,22 @@ impl Client {
/// * compatible with user (certificate) and guest (certificate-less) connection types
pub fn request_async(
&self,
uri: Uri,
request: Request,
priority: Priority,
cancellable: Cancellable,
certificate: Option<TlsCertificate>,
callback: impl Fn(Result<connection::Response, Error>) + 'static,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
// Begin new connection
// * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid
// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) {
match crate::gio::network_address::from_uri(
&match request {
Request::Gemini(ref request) => request.uri.clone(),
Request::Titan(ref request) => request.uri.clone(),
},
crate::DEFAULT_PORT,
) {
Ok(network_address) => {
self.socket
.connect_async(&network_address.clone(), Some(&cancellable.clone()), {
@ -78,15 +84,27 @@ impl Client {
Some(network_address),
is_session_resumption,
) {
Ok(connection) => connection.request_async(
uri.to_string(),
priority,
cancellable,
move |result| match result {
Ok(response) => callback(Ok(response)),
Err(e) => callback(Err(Error::Connection(e))),
},
),
Ok(connection) => match request {
Request::Gemini(request) => connection
.gemini_request_async(
request,
priority,
cancellable,
move |result| match result {
Ok(response) => callback(Ok(response)),
Err(e) => callback(Err(Error::Connection(e))),
},
),
Request::Titan(request) => connection.titan_request_async(
request,
priority,
cancellable,
move |result| match result {
Ok(response) => callback(Ok(response)),
Err(e) => callback(Err(Error::Connection(e))),
},
),
},
Err(e) => callback(Err(Error::Connection(e))),
}
}

View file

@ -1,7 +1,9 @@
pub mod error;
pub mod request;
pub mod response;
pub use error::Error;
pub use request::{Gemini, Request, Titan};
pub use response::Response;
use gio::{
@ -46,18 +48,69 @@ impl Connection {
// Actions
/// Make new request to `Self` connection
/// * callback with new `Response` on success or `Error` on failure
/// Send new `Request` to `Self` connection using
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
pub fn request_async(
self,
query: String,
request: Request,
priority: Priority,
cancellable: Cancellable,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
match request {
Request::Gemini(request) => {
self.gemini_request_async(request, priority, cancellable, callback)
}
Request::Titan(request) => {
self.titan_request_async(request, priority, cancellable, callback)
}
}
}
/// Make new request to `Self` connection using
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol
/// * callback with new `Response` on success or `Error` on failure
/// * see also `request_async` method to send multi-protocol requests
pub fn gemini_request_async(
self,
request: Gemini,
priority: Priority,
cancellable: Cancellable,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback);
}
/// Make new request to `Self` connection using
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
/// * callback with new `Response` on success or `Error` on failure
/// * see also `request_async` method to send multi-protocol requests
pub fn titan_request_async(
self,
request: Titan,
priority: Priority,
cancellable: Cancellable,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback);
}
/// Low-level shared method to send raw bytes array over
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
/// * bytes array should include formatted header according to protocol selected
/// * for high-level requests see `gemini_request_async` and `titan_request_async` methods
/// * to construct multi-protocol request with single function, use `request_async` method
pub fn bytes_request_async(
self,
request: &Bytes,
priority: Priority,
cancellable: Cancellable,
callback: impl Fn(Result<Response, Error>) + 'static,
) {
// Send request
self.stream().output_stream().write_bytes_async(
&Bytes::from(format!("{query}\r\n").as_bytes()),
request,
priority,
Some(&cancellable.clone()),
move |result| match result {

View file

@ -0,0 +1,10 @@
pub mod gemini;
pub mod titan;
pub use gemini::Gemini;
pub use titan::Titan;
pub enum Request {
Gemini(Gemini),
Titan(Titan),
}

View file

@ -0,0 +1,22 @@
use glib::{Bytes, Uri};
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol enum object for `Request`
pub struct Gemini {
pub uri: Uri,
}
impl Gemini {
// Constructors
/// Build valid new `Self`
pub fn build(uri: Uri) -> Self {
Self { uri } // @TODO validate
}
// Getters
/// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
pub fn to_bytes(&self) -> Bytes {
Bytes::from(format!("{}\r\n", self.uri).as_bytes())
}
}

View file

@ -0,0 +1,53 @@
use glib::{Bytes, Uri};
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request`
pub struct Titan {
pub uri: Uri,
pub size: usize,
pub mime: String,
pub token: Option<String>,
pub data: Vec<u8>,
}
impl Titan {
// Constructors
/// Build valid new `Self`
pub fn build(
uri: Uri,
size: usize,
mime: String,
token: Option<String>,
data: Vec<u8>,
) -> Self {
Self {
uri,
size,
mime,
token,
data,
} // @TODO validate
}
// Getters
/// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
pub fn to_bytes(&self) -> Bytes {
// Build header
let mut header = format!("{};size={};mime={}", self.uri, self.size, self.mime);
if let Some(ref token) = self.token {
header.push_str(&format!(";token={token}"));
}
header.push_str("\r\n");
let header_bytes = header.into_bytes();
// Build request
let mut bytes: Vec<u8> = Vec::with_capacity(self.size + header_bytes.len());
bytes.extend(header_bytes);
bytes.extend(&self.data);
// Wrap result
Bytes::from(&bytes)
}
}