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::error::GementionError;
use crate::models::mentions::{Mention, MentionUpload}; use crate::models::mentions::{C2SMentionRequest, Mention};
use crate::models::web::MentionResponse; use crate::models::web::MentionResponse;
use crate::{verification::verify}; use crate::validation::Validation;
use crate::verification::verify;
use fluffer::Client; use fluffer::Client;
// Receive a mention over Titan. /// Receive a client-to-server Gemention: i.e. a user posting a
// - Make Gemini request to see if target page supports gemention. /// comment to a gemention-enabled page. The client must identify
// - If so, store mention in DB. /// themselves with a cert that at least has a username.
pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> { pub(crate) async fn receive_c2s_gemention(
// TODO change to return MentionResponse or something like that. client: Client,
let titan = MentionUpload::try_from(&client)?; ) -> Result<MentionResponse, GementionError> {
let mut mention = Mention::try_from(titan)?; let mention_upload = C2SMentionRequest::try_from(&client)?.validate()?;
verify(&mut mention).await?; 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)) Ok(MentionResponse::from(mention))
} }

View File

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

View File

@ -3,6 +3,7 @@ use fluffer::AppErr;
mod comments; mod comments;
mod error; mod error;
mod models; mod models;
mod validation;
mod routes; mod routes;
mod verification; 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 /// Wraps an incoming Titan request and provides handy methods for
/// extracting the expected information. /// extracting the expected information for a client-to-server
pub(crate) struct MentionUpload<'a> { /// gemention.
pub(crate) struct C2SMentionRequest<'a> {
client: &'a Client, client: &'a Client,
receiving_endpoint: &'a Url,
} }
impl<'a> MentionUpload<'a> { impl<'a> C2SMentionRequest<'a> {
pub fn from_client(client: &'a Client) -> Result<MentionUpload, GementionError> { pub fn from_client(client: &'a Client) -> Result<C2SMentionRequest, GementionError> {
if let Some(_) = client.titan { if let Some(_) = client.titan {
Ok(Self { client: &client }) Ok(Self {
client: &client,
receiving_endpoint: &client.url,
})
} else { } else {
Err(GementionError::NotTitanResource) Err(GementionError::NotTitanResource)
} }
@ -59,6 +64,10 @@ impl<'a> MentionUpload<'a> {
titan!(self).size titan!(self).size
} }
pub fn certificate(&self) -> Option<String> {
self.client.certificate()
}
pub fn fingerprint(&self) -> Option<String> { pub fn fingerprint(&self) -> Option<String> {
self.client.fingerprint() self.client.fingerprint()
} }
@ -74,13 +83,17 @@ impl<'a> MentionUpload<'a> {
pub fn content(&self) -> Result<&str, GementionError> { pub fn content(&self) -> Result<&str, GementionError> {
parse_content_body(self.raw_body()) 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; type Error = GementionError;
fn try_from(client: &'a Client) -> Result<Self, Self::Error> { 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; 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" { if resource.mime() == "text/plain" {
// Be flexible on mention type: if first line isn't a // Be flexible on mention type: if first line isn't a
// mention type, just assume reply. // mention type, just assume reply.

View File

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

View File

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

View File

@ -1,10 +1,11 @@
use fluffer::App; use fluffer::App;
use crate::comments::receive_mention; use crate::comments::{receive_c2s_gemention, receive_s2s_gemention};
pub(crate) fn create_app() -> App { pub(crate) fn create_app() -> App {
App::default() 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 { .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 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 resp = germ_request(&url).await?;
let meta = GeminiMetadata::from_string(resp.meta()); let meta = GeminiMetadata::from_string(resp.meta());
@ -89,6 +89,6 @@ async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
Ok(()) Ok(())
} }
pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> { pub(crate) async fn verify(expected_url: &Url, mention: &mut Mention) -> Result<(), GementionError> {
verify_mention(mention).await verify_mention(expected_url, mention).await
} }