Compare commits

..

No commits in common. "c5eeb147a5444ba52329a5766a38508fd2107164" and "72a3c2f3cf487db9c22f587755c6fcfcb50016b5" have entirely different histories.

10 changed files with 44 additions and 144 deletions

View File

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

View File

@ -4,8 +4,6 @@ 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")]
@ -32,9 +30,6 @@ 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,7 +3,6 @@ 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

@ -1,7 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use fluffer::{Client, GemBytes}; use fluffer::{Client, GemBytes};
use std::fmt::Display; use std::fmt::Display;
use url::Url;
use crate::error::GementionError; use crate::error::GementionError;
@ -29,20 +28,15 @@ 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 for a client-to-server /// extracting the expected information.
/// gemention. pub(crate) struct MentionUpload<'a> {
pub(crate) struct C2SMentionRequest<'a> {
client: &'a Client, client: &'a Client,
accessed_url: &'a Url,
} }
impl<'a> C2SMentionRequest<'a> { impl<'a> MentionUpload<'a> {
pub fn from_client(client: &'a Client) -> Result<C2SMentionRequest, GementionError> { pub fn from_client(client: &'a Client) -> Result<MentionUpload, GementionError> {
if let Some(_) = client.titan { if let Some(_) = client.titan {
Ok(Self { Ok(Self { client: &client })
client: &client,
accessed_url: &client.url,
})
} else { } else {
Err(GementionError::NotTitanResource) Err(GementionError::NotTitanResource)
} }
@ -64,10 +58,6 @@ impl<'a> C2SMentionRequest<'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()
} }
@ -83,18 +73,13 @@ impl<'a> C2SMentionRequest<'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())
} }
/// The full URL that was accessed for the incoming request.
pub fn accessed_url(&self) -> &Url {
self.accessed_url
}
} }
impl<'a> TryFrom<&'a Client> for C2SMentionRequest<'a> { impl<'a> TryFrom<&'a Client> for MentionUpload<'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> {
C2SMentionRequest::from_client(client) MentionUpload::from_client(client)
} }
} }
@ -106,7 +91,6 @@ pub(crate) enum MentionType {
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Mention { pub(crate) struct Mention {
receiving_endpoint: Url,
mention_type: MentionType, mention_type: MentionType,
target: String, target: String,
user: String, user: String,
@ -119,14 +103,6 @@ impl Mention {
&self.target &self.target
} }
pub fn receiving_endpoint(&self) -> &Url {
&self.receiving_endpoint
}
pub fn target_url(&self) -> Url {
Url::parse(&format!("gemini://{}", &self.target)).unwrap()
}
pub fn mention_type(&self) -> &MentionType { pub fn mention_type(&self) -> &MentionType {
&self.mention_type &self.mention_type
} }
@ -149,13 +125,12 @@ impl Mention {
fn valid_gemtext(&self) -> String { fn valid_gemtext(&self) -> String {
let headline = format!("## {} from {}", self.mention_type, self.user); let headline = format!("## {} from {}", self.mention_type, self.user);
let header = format!("=> {} In response to this page", self.target_url());
let content = match &self.mention_type { let content = match &self.mention_type {
MentionType::Reply => self.content.clone().unwrap_or(String::from("[no content]")), MentionType::Reply => self.content.clone().unwrap_or(String::from("[no content]")),
MentionType::Like => format!("{} liked this.", self.user), MentionType::Like => format!("{} liked this.", self.user),
}; };
format!("{}\n\n{}\n\n### Content\n{}", headline, header, content) format!("{}\n\n{}", headline, content)
} }
fn invalid_gemtext(&self) -> String { fn invalid_gemtext(&self) -> String {
@ -207,10 +182,10 @@ impl TryFrom<&[u8]> for MentionType {
} }
} }
impl<'a> TryFrom<C2SMentionRequest<'a>> for Mention { impl<'a> TryFrom<MentionUpload<'a>> for Mention {
type Error = GementionError; type Error = GementionError;
fn try_from(resource: C2SMentionRequest<'a>) -> Result<Self, Self::Error> { fn try_from(resource: MentionUpload<'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.
@ -229,15 +204,6 @@ impl<'a> TryFrom<C2SMentionRequest<'a>> for Mention {
.username() .username()
.ok_or(GementionError::UsernameNotProvided)?; .ok_or(GementionError::UsernameNotProvided)?;
let receiving_endpoint = Url::parse(
resource
.accessed_url()
.as_str()
.split(&target)
.next()
.ok_or(GementionError::InvalidBody)?,
)?;
let content = resource.content()?.to_owned(); let content = resource.content()?.to_owned();
let content = if !content.is_empty() { let content = if !content.is_empty() {
Some(content) Some(content)
@ -250,7 +216,6 @@ impl<'a> TryFrom<C2SMentionRequest<'a>> for Mention {
mention_type, mention_type,
content, content,
target, target,
receiving_endpoint,
verification_status: VerificationStatus::NotYetVerified, verification_status: VerificationStatus::NotYetVerified,
}) })
} else { } else {

View File

@ -1,6 +1,8 @@
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 {
@ -48,7 +50,7 @@ pub(crate) enum VerificationFailureReason {
/// One or more mention links exist, but they are not to this /// One or more mention links exist, but they are not to this
/// endpoint, or for this page. /// endpoint, or for this page.
MentionLinkIncorrect(String), MentionLinkIncorrect,
/// There was an error during the verification process. /// There was an error during the verification process.
Error(GementionError), Error(GementionError),
@ -58,9 +60,7 @@ impl ToString for VerificationFailureReason {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Self::NoMentionLinkFound => String::from("No mention link found"), Self::NoMentionLinkFound => String::from("No mention link found"),
Self::MentionLinkIncorrect(endpoint) => { Self::MentionLinkIncorrect => String::from("Mention link points to wrong target"),
format!("Target URL does not accept gementions from endpoint: {}", endpoint)
}
Self::Error(err) => err.to_string(), Self::Error(err) => err.to_string(),
} }
} }

View File

@ -12,7 +12,7 @@ impl From<Mention> for MentionResponse {
match mention.verify_status() { match mention.verify_status() {
VerificationStatus::Verified { .. } => Self::VerifiedMention(mention), VerificationStatus::Verified { .. } => Self::VerifiedMention(mention),
VerificationStatus::Invalid(_) => Self::InvalidMention(mention), VerificationStatus::Invalid(_) => Self::InvalidMention(mention),
VerificationStatus::NotYetVerified => Self::InvalidMention(mention), VerificationStatus::NotYetVerified => Self::InvalidMention(mention)
} }
} }
} }
@ -45,11 +45,9 @@ impl MentionResponse {
pub fn description(&self) -> String { pub fn description(&self) -> String {
match self { match self {
Self::VerifiedMention(_) => { Self::VerifiedMention(_) => "You have successfully submitted a gemention.",
"You have successfully submitted a gemention. It is shown below."
}
Self::InvalidMention(_) => { Self::InvalidMention(_) => {
"There was an error submitting the gemention. The error is detailed below." "There was an error submitting the gemention, detailed below."
} }
Self::NotTitanRequest => "Your request was not a Titan protocol request.", Self::NotTitanRequest => "Your request was not a Titan protocol request.",
} }
@ -70,7 +68,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,11 +1,10 @@
use fluffer::App; use fluffer::App;
use crate::comments::{receive_c2s_gemention, receive_s2s_gemention}; use crate::comments::receive_mention;
pub(crate) fn create_app() -> App { pub(crate) fn create_app() -> App {
App::default() App::default()
.titan("/c2s/receive/*target", receive_c2s_gemention, 2048) .titan("/receive/*target", receive_mention, 20_000)
.titan("/s2s/receive/*target", receive_s2s_gemention, 2048)
.route("/", |_| async { .route("/", |_| async {
"# Welcome\n=> titan://localhost/c2s/receive/agnos.is/posts/webmentions-test.gmi Receive Mention" "# Welcome\n=> titan://localhost/receive/agnos.is/posts/webmentions-test.gmi Receive Mention"
}) })
} }

View File

@ -1,28 +0,0 @@
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)
}
}

View File

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

View File

@ -7,12 +7,13 @@ use crate::error::GementionError;
use crate::models::mentions::Mention; use crate::models::mentions::Mention;
use crate::models::verification::*; use crate::models::verification::*;
fn is_mention_link(endpoint: &Url, gemtext_link: &str) -> bool { const OUR_ENDPOINT: &'static str = "titan://localhost/receive/";
gemtext_link.starts_with(endpoint.as_str())
fn is_mention_link(gemtext_link: &str) -> bool {
gemtext_link.starts_with(OUR_ENDPOINT)
} }
fn scan_for_gemention_endpoints( fn scan_for_gemention_endpoints(
endpoint: &Url,
meta: GeminiMetadata, meta: GeminiMetadata,
ast: GeminiAst, ast: GeminiAst,
) -> (VerificationSource, Vec<String>) { ) -> (VerificationSource, Vec<String>) {
@ -28,7 +29,7 @@ fn scan_for_gemention_endpoints(
.inner() .inner()
.into_iter() .into_iter()
.filter_map(|node| match node { .filter_map(|node| match node {
GemtextNode::Link { ref to, .. } if is_mention_link(endpoint, to) => Some(to), GemtextNode::Link { ref to, .. } if is_mention_link(to) => Some(to),
_ => None, _ => None,
}) })
.cloned() .cloned()
@ -37,17 +38,14 @@ fn scan_for_gemention_endpoints(
(VerificationSource::Page, endpoints) (VerificationSource::Page, endpoints)
} }
fn verify_mentions( fn verify_mentions<S: AsRef<str>>(
mention: &mut Mention, expected_link: S,
source_and_endpoints: (VerificationSource, Vec<String>), source_and_endpoints: (VerificationSource, Vec<String>),
) -> Result<VerificationStatus, GementionError> { ) -> VerificationStatus {
let (verification_source, mentions) = source_and_endpoints; let (verification_source, mentions) = source_and_endpoints;
// TODO need to normalize url from page as well as ours, to make
// sure things like ports being in url or not are handled.
let expected_link = mention.receiving_endpoint().join(mention.target())?;
let expected_link = expected_link.as_ref(); let expected_link = expected_link.as_ref();
let verification_status = if mentions.len() > 0 { if mentions.len() > 0 {
// We have links that go to our endpoint. Scan links for the // We have links that go to our endpoint. Scan links for the
// one we expect (i.e. for the target), otherwise we say // one we expect (i.e. for the target), otherwise we say
// incorrect link. // incorrect link.
@ -64,19 +62,16 @@ fn verify_mentions(
} }
}) })
.unwrap_or(VerificationStatus::Invalid( .unwrap_or(VerificationStatus::Invalid(
VerificationFailureReason::MentionLinkIncorrect( VerificationFailureReason::MentionLinkIncorrect,
mention.receiving_endpoint().to_string(),
),
)) ))
} else { } else {
VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound) VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound)
}; }
Ok(verification_status)
} }
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> { async fn verify_mention(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())?;
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());
@ -87,9 +82,8 @@ async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
.ok_or(GementionError::NoContentFoundForTarget)?; .ok_or(GementionError::NoContentFoundForTarget)?;
let ast = GeminiAst::from_string(content); let ast = GeminiAst::from_string(content);
let source_and_endpoints = let source_and_endpoints = scan_for_gemention_endpoints(meta, ast);
scan_for_gemention_endpoints(mention.receiving_endpoint(), meta, ast); let status = verify_mentions(expected_link, source_and_endpoints);
let status = verify_mentions(mention, source_and_endpoints)?;
mention.set_verify_status(status); mention.set_verify_status(status);
Ok(()) Ok(())