mirror of
https://github.com/YGGverse/ggemini.git
synced 2026-03-31 09:05:45 +00:00
implement Titan protocol features
This commit is contained in:
parent
66a0de6a8e
commit
29b835411d
8 changed files with 204 additions and 27 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ggemini"
|
name = "ggemini"
|
||||||
version = "0.13.1"
|
version = "0.14.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "Glib/Gio-oriented network API for Gemini protocol"
|
description = "Glib/Gio-oriented network API for Gemini protocol"
|
||||||
keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"]
|
keywords = ["gemini", "titan", "glib", "gio", "client"]
|
||||||
categories = ["development-tools", "network-programming", "parsing"]
|
categories = ["development-tools", "network-programming", "parsing"]
|
||||||
repository = "https://github.com/YGGverse/ggemini"
|
repository = "https://github.com/YGGverse/ggemini"
|
||||||
|
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -25,20 +25,25 @@ cargo add ggemini
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
``` rust
|
``` rust
|
||||||
use gtk::gio::*;
|
use gio::*;
|
||||||
use gtk::glib::*;
|
use glib::*;
|
||||||
|
|
||||||
use ggemini::client::{
|
use ggemini::client::{
|
||||||
connection::{
|
connection::{
|
||||||
response::meta::{Mime, Status},
|
Request, Response,
|
||||||
Response,
|
request::Gemini,
|
||||||
|
response::meta::{Mime, Status}
|
||||||
},
|
},
|
||||||
Client, Error,
|
Client, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
Client::new().request_async(
|
Client::new().request_async(
|
||||||
Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
|
Request::Gemini(
|
||||||
|
Gemini::build(
|
||||||
|
Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap()
|
||||||
|
)
|
||||||
|
),
|
||||||
Priority::DEFAULT,
|
Priority::DEFAULT,
|
||||||
Cancellable::new(),
|
Cancellable::new(),
|
||||||
None, // optional `GTlsCertificate`
|
None, // optional `GTlsCertificate`
|
||||||
|
|
@ -63,6 +68,8 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## See also
|
* to send requests using Titan protocol, see also `titan_request_async` implementation
|
||||||
|
|
||||||
|
## Other crates
|
||||||
|
|
||||||
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API
|
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
pub use connection::Connection;
|
pub use connection::{Connection, Request, Response};
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
|
||||||
use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate};
|
use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate};
|
||||||
use glib::{Priority, Uri};
|
use glib::Priority;
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
|
|
||||||
|
|
@ -56,16 +56,22 @@ impl Client {
|
||||||
/// * compatible with user (certificate) and guest (certificate-less) connection types
|
/// * compatible with user (certificate) and guest (certificate-less) connection types
|
||||||
pub fn request_async(
|
pub fn request_async(
|
||||||
&self,
|
&self,
|
||||||
uri: Uri,
|
request: Request,
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
certificate: Option<TlsCertificate>,
|
certificate: Option<TlsCertificate>,
|
||||||
callback: impl Fn(Result<connection::Response, Error>) + 'static,
|
callback: impl Fn(Result<Response, Error>) + 'static,
|
||||||
) {
|
) {
|
||||||
// Begin new connection
|
// Begin new connection
|
||||||
// * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid
|
// * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid
|
||||||
// [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
|
// [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) => {
|
Ok(network_address) => {
|
||||||
self.socket
|
self.socket
|
||||||
.connect_async(&network_address.clone(), Some(&cancellable.clone()), {
|
.connect_async(&network_address.clone(), Some(&cancellable.clone()), {
|
||||||
|
|
@ -78,15 +84,27 @@ impl Client {
|
||||||
Some(network_address),
|
Some(network_address),
|
||||||
is_session_resumption,
|
is_session_resumption,
|
||||||
) {
|
) {
|
||||||
Ok(connection) => connection.request_async(
|
Ok(connection) => match request {
|
||||||
uri.to_string(),
|
Request::Gemini(request) => connection
|
||||||
priority,
|
.gemini_request_async(
|
||||||
cancellable,
|
request,
|
||||||
move |result| match result {
|
priority,
|
||||||
Ok(response) => callback(Ok(response)),
|
cancellable,
|
||||||
Err(e) => callback(Err(Error::Connection(e))),
|
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))),
|
Err(e) => callback(Err(Error::Connection(e))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use request::{Gemini, Request, Titan};
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
|
|
||||||
use gio::{
|
use gio::{
|
||||||
|
|
@ -46,18 +48,69 @@ impl Connection {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
/// Make new request to `Self` connection
|
/// Send new `Request` to `Self` connection using
|
||||||
/// * callback with new `Response` on success or `Error` on failure
|
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or
|
||||||
|
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol
|
||||||
pub fn request_async(
|
pub fn request_async(
|
||||||
self,
|
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,
|
priority: Priority,
|
||||||
cancellable: Cancellable,
|
cancellable: Cancellable,
|
||||||
callback: impl Fn(Result<Response, Error>) + 'static,
|
callback: impl Fn(Result<Response, Error>) + 'static,
|
||||||
) {
|
) {
|
||||||
// Send request
|
|
||||||
self.stream().output_stream().write_bytes_async(
|
self.stream().output_stream().write_bytes_async(
|
||||||
&Bytes::from(format!("{query}\r\n").as_bytes()),
|
request,
|
||||||
priority,
|
priority,
|
||||||
Some(&cancellable.clone()),
|
Some(&cancellable.clone()),
|
||||||
move |result| match result {
|
move |result| match result {
|
||||||
|
|
|
||||||
10
src/client/connection/request.rs
Normal file
10
src/client/connection/request.rs
Normal 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),
|
||||||
|
}
|
||||||
22
src/client/connection/request/gemini.rs
Normal file
22
src/client/connection/request/gemini.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/client/connection/request/titan.rs
Normal file
53
src/client/connection/request/titan.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,15 @@
|
||||||
|
use gio::*;
|
||||||
|
use glib::*;
|
||||||
|
|
||||||
|
use ggemini::client::connection::request::Gemini;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_connection_request_gemini_build() {
|
||||||
|
const REQUEST: &str = "gemini://geminiprotocol.net/";
|
||||||
|
|
||||||
|
let request = Gemini::build(Uri::parse(REQUEST, UriFlags::NONE).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(&request.uri.to_string(), REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
// @TODO
|
// @TODO
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue