Split into c2s and s2s endpoints

This commit is contained in:
projectmoon 2024-03-31 12:02:46 +02:00
parent 6a73ee7a43
commit 8f6e7a0d02
10 changed files with 97 additions and 29 deletions

View File

@ -1,17 +1,32 @@
use crate::error::GementionError;
use crate::models::mentions::{Mention, MentionUpload};
use crate::models::mentions::{C2SMentionRequest, Mention};
use crate::models::web::MentionResponse;
use crate::{verification::verify};
use crate::validation::Validation;
use crate::verification::verify;
use fluffer::Client;
// Receive a mention over Titan.
// - Make Gemini request to see if target page supports gemention.
// - If so, store mention in DB.
pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> {
// TODO change to return MentionResponse or something like that.
let titan = MentionUpload::try_from(&client)?;
let mut mention = Mention::try_from(titan)?;
verify(&mut mention).await?;
/// Receive a client-to-server Gemention: i.e. a user posting a
/// comment to a gemention-enabled page. The client must identify
/// themselves with a cert that at least has a username.
pub(crate) async fn receive_c2s_gemention(
client: Client,
) -> Result<MentionResponse, GementionError> {
let mention_upload = C2SMentionRequest::try_from(&client)?.validate()?;
let mut mention = Mention::try_from(mention_upload)?;
verify(&client.url, &mut mention).await?;
Ok(MentionResponse::from(mention))
}
/// Receive a server-to-server Gemention: another capsule linking to a
/// gemention-enabled page on this capsule. There is an exchange of
/// titan keys so that the servers can talk to one another.
pub(crate) async fn receive_s2s_gemention(
client: Client,
) -> Result<MentionResponse, GementionError> {
let mention_upload = C2SMentionRequest::try_from(&client)?;
let mut mention = Mention::try_from(mention_upload)?;
verify(&client.url, &mut mention).await?;
Ok(MentionResponse::from(mention))
}

View File

@ -4,6 +4,8 @@ use async_trait::async_trait;
use fluffer::GemBytes;
use thiserror::Error;
use crate::validation::c2s::C2SValidationError;
#[derive(Error, Debug)]
pub(crate) enum GementionError {
#[error("No content found for target")]
@ -30,6 +32,9 @@ pub(crate) enum GementionError {
#[error("value was not utf8: {0}")]
Utf8Error(#[from] Utf8Error),
#[error("c2s validation error: {0}")]
C2SValidationError(#[from] C2SValidationError),
#[error("generic error: {0}")]
UnclassifiedError(#[from] anyhow::Error),
}

View File

@ -3,6 +3,7 @@ use fluffer::AppErr;
mod comments;
mod error;
mod models;
mod validation;
mod routes;
mod verification;

View File

@ -29,15 +29,20 @@ fn parse_content_body(raw_body: &[u8]) -> Result<&str, GementionError> {
}
/// Wraps an incoming Titan request and provides handy methods for
/// extracting the expected information.
pub(crate) struct MentionUpload<'a> {
/// extracting the expected information for a client-to-server
/// gemention.
pub(crate) struct C2SMentionRequest<'a> {
client: &'a Client,
receiving_endpoint: &'a Url,
}
impl<'a> MentionUpload<'a> {
pub fn from_client(client: &'a Client) -> Result<MentionUpload, GementionError> {
impl<'a> C2SMentionRequest<'a> {
pub fn from_client(client: &'a Client) -> Result<C2SMentionRequest, GementionError> {
if let Some(_) = client.titan {
Ok(Self { client: &client })
Ok(Self {
client: &client,
receiving_endpoint: &client.url,
})
} else {
Err(GementionError::NotTitanResource)
}
@ -59,6 +64,10 @@ impl<'a> MentionUpload<'a> {
titan!(self).size
}
pub fn certificate(&self) -> Option<String> {
self.client.certificate()
}
pub fn fingerprint(&self) -> Option<String> {
self.client.fingerprint()
}
@ -74,13 +83,17 @@ impl<'a> MentionUpload<'a> {
pub fn content(&self) -> Result<&str, GementionError> {
parse_content_body(self.raw_body())
}
pub fn receiving_endpoint(&self) -> &Url {
self.receiving_endpoint
}
}
impl<'a> TryFrom<&'a Client> for MentionUpload<'a> {
impl<'a> TryFrom<&'a Client> for C2SMentionRequest<'a> {
type Error = GementionError;
fn try_from(client: &'a Client) -> Result<Self, Self::Error> {
MentionUpload::from_client(client)
C2SMentionRequest::from_client(client)
}
}
@ -188,10 +201,10 @@ impl TryFrom<&[u8]> for MentionType {
}
}
impl<'a> TryFrom<MentionUpload<'a>> for Mention {
impl<'a> TryFrom<C2SMentionRequest<'a>> for Mention {
type Error = GementionError;
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
fn try_from(resource: C2SMentionRequest<'a>) -> Result<Self, Self::Error> {
if resource.mime() == "text/plain" {
// Be flexible on mention type: if first line isn't a
// mention type, just assume reply.

View File

@ -1,8 +1,6 @@
use crate::error::GementionError;
use std::fmt::Display;
use super::mentions::Mention;
#[derive(Debug)]
pub(crate) enum VerificationStatus {
Verified {

View File

@ -70,7 +70,7 @@ impl GemBytes for MentionResponse {
let status_line = self.status_line();
let body = if self.should_have_body() {
format!(
"{}\n\n{}\n\n{},",
"{}\n\n{}\n\n{}",
self.headline(),
self.description(),
self.mention_body()

View File

@ -1,10 +1,11 @@
use fluffer::App;
use crate::comments::receive_mention;
use crate::comments::{receive_c2s_gemention, receive_s2s_gemention};
pub(crate) fn create_app() -> App {
App::default()
.titan("/receive/*target", receive_mention, 20_000)
.titan("/c2s/receive/*target", receive_c2s_gemention, 2048)
.titan("/s2s/receive/*target", receive_s2s_gemention, 2048)
.route("/", |_| async {
"# Welcome\n=> titan://localhost/receive/agnos.is/posts/webmentions-test.gmi Receive Mention"
"# Welcome\n=> titan://localhost/c2s/receive/agnos.is/posts/webmentions-test.gmi Receive Mention"
})
}

28
src/validation/c2s.rs Normal file
View File

@ -0,0 +1,28 @@
use crate::{models::mentions::C2SMentionRequest, error::GementionError};
use super::Validation;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum C2SValidationError {
#[error("a username is required on the client certificate")]
UsernameRequired,
#[error("a client certificate is required")]
ClientCertificateRequired,
}
impl Validation for C2SMentionRequest<'_> {
type Error = C2SValidationError;
fn validate(self) -> Result<Self, Self::Error> {
if self.username().is_none() {
return Err(C2SValidationError::UsernameRequired);
}
if self.certificate().is_none() {
return Err(C2SValidationError::ClientCertificateRequired);
}
Ok(self)
}
}

7
src/validation/mod.rs Normal file
View File

@ -0,0 +1,7 @@
pub(crate) mod c2s;
pub trait Validation {
type Error;
fn validate(self) -> Result<Self, Self::Error> where Self: Sized;
}

View File

@ -69,9 +69,9 @@ fn verify_mentions<S: AsRef<str>>(
}
}
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
async fn verify_mention(expected_link: &Url, mention: &mut Mention) -> Result<(), GementionError> {
let url = Url::parse(&format!("gemini://{}", mention.target()))?;
let expected_link = Url::parse(OUR_ENDPOINT)?.join(mention.target())?;
println!("expected link is {}", expected_link);
let resp = germ_request(&url).await?;
let meta = GeminiMetadata::from_string(resp.meta());
@ -89,6 +89,6 @@ async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
Ok(())
}
pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> {
verify_mention(mention).await
pub(crate) async fn verify(expected_url: &Url, mention: &mut Mention) -> Result<(), GementionError> {
verify_mention(expected_url, mention).await
}