From 8f6e7a0d02d7b1dc04d25d51bb18ee2ccc8ffdf0 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sun, 31 Mar 2024 12:02:46 +0200 Subject: [PATCH] Split into c2s and s2s endpoints --- src/comments.rs | 35 +++++++++++++++++++++++++---------- src/error.rs | 5 +++++ src/main.rs | 1 + src/models/mentions.rs | 31 ++++++++++++++++++++++--------- src/models/verification.rs | 2 -- src/models/web.rs | 2 +- src/routes.rs | 7 ++++--- src/validation/c2s.rs | 28 ++++++++++++++++++++++++++++ src/validation/mod.rs | 7 +++++++ src/verification/mod.rs | 8 ++++---- 10 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 src/validation/c2s.rs create mode 100644 src/validation/mod.rs diff --git a/src/comments.rs b/src/comments.rs index be945d8..1584f23 100644 --- a/src/comments.rs +++ b/src/comments.rs @@ -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 { - // 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 { + 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 { + 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)) } diff --git a/src/error.rs b/src/error.rs index 56f241c..2e8f269 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } diff --git a/src/main.rs b/src/main.rs index 050ad2a..7f31888 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use fluffer::AppErr; mod comments; mod error; mod models; +mod validation; mod routes; mod verification; diff --git a/src/models/mentions.rs b/src/models/mentions.rs index d5cbee5..9217d3e 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -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 { +impl<'a> C2SMentionRequest<'a> { + pub fn from_client(client: &'a Client) -> Result { 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 { + self.client.certificate() + } + pub fn fingerprint(&self) -> Option { 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 { - MentionUpload::from_client(client) + C2SMentionRequest::from_client(client) } } @@ -188,10 +201,10 @@ impl TryFrom<&[u8]> for MentionType { } } -impl<'a> TryFrom> for Mention { +impl<'a> TryFrom> for Mention { type Error = GementionError; - fn try_from(resource: MentionUpload<'a>) -> Result { + fn try_from(resource: C2SMentionRequest<'a>) -> Result { if resource.mime() == "text/plain" { // Be flexible on mention type: if first line isn't a // mention type, just assume reply. diff --git a/src/models/verification.rs b/src/models/verification.rs index 1d98070..3b28baf 100644 --- a/src/models/verification.rs +++ b/src/models/verification.rs @@ -1,8 +1,6 @@ use crate::error::GementionError; use std::fmt::Display; -use super::mentions::Mention; - #[derive(Debug)] pub(crate) enum VerificationStatus { Verified { diff --git a/src/models/web.rs b/src/models/web.rs index 1d4d5f7..952432d 100644 --- a/src/models/web.rs +++ b/src/models/web.rs @@ -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() diff --git a/src/routes.rs b/src/routes.rs index c04d85d..f257fbc 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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" }) } diff --git a/src/validation/c2s.rs b/src/validation/c2s.rs new file mode 100644 index 0000000..8abe38e --- /dev/null +++ b/src/validation/c2s.rs @@ -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 { + if self.username().is_none() { + return Err(C2SValidationError::UsernameRequired); + } + + if self.certificate().is_none() { + return Err(C2SValidationError::ClientCertificateRequired); + } + + Ok(self) + } +} diff --git a/src/validation/mod.rs b/src/validation/mod.rs new file mode 100644 index 0000000..c211e23 --- /dev/null +++ b/src/validation/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod c2s; + +pub trait Validation { + type Error; + + fn validate(self) -> Result where Self: Sized; +} diff --git a/src/verification/mod.rs b/src/verification/mod.rs index b5f21f6..021a7aa 100644 --- a/src/verification/mod.rs +++ b/src/verification/mod.rs @@ -69,9 +69,9 @@ fn verify_mentions>( } } -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 }