Gemention/src/verification/mod.rs

115 lines
3.8 KiB
Rust

use germ::ast::{Ast as GeminiAst, Node as GemtextNode};
use germ::meta::Meta as GeminiMetadata;
use germ::request::request as germ_request;
use url::Url;
use crate::error::GementionError;
use crate::models::mentions::Mention;
use crate::models::verification::*;
/// Normalize URL by making sure it starts with the right protocol and
/// has a port number.
fn normalize_url(url: &mut Url) {
if url.port().is_none() {
// Weird line because the error is Unit type.
let _ = url.set_port(Some(1965));
}
}
fn is_mention_link(endpoint: &Url, gemtext_link: &str) -> bool {
gemtext_link.starts_with(endpoint.as_str())
}
fn scan_for_gemention_endpoints(
endpoint: &Url,
meta: GeminiMetadata,
ast: GeminiAst,
) -> (VerificationSource, Vec<String>) {
// TODO remove this in favor of checking for <domain>/gemention.txt
// Check metadata of the page for a gemention endpoint.
if let Some(endpoint) = meta.parameters().get("gemention") {
let endpoint = endpoint.trim_start_matches("=");
return (VerificationSource::Meta, vec![endpoint.to_owned()]);
}
// If that fails, check the page itself for the first available
// link that matches.
let endpoints = ast
.inner()
.into_iter()
.filter_map(|node| match node {
GemtextNode::Link { ref to, .. } if is_mention_link(endpoint, to) => Some(to),
_ => None,
})
.cloned()
.collect();
(VerificationSource::Page, endpoints)
}
fn verify_mentions(
mention: &mut Mention,
source_and_endpoints: (VerificationSource, Vec<String>),
) -> Result<VerificationStatus, GementionError> {
let (verification_source, mentions) = source_and_endpoints;
let mut expected_link = mention.receiving_endpoint().join(mention.target())?;
normalize_url(&mut expected_link);
let verification_status = if mentions.len() > 0 {
// We have links that go to our endpoint. Scan links for the
// one we expect (i.e. for the target), otherwise we say
// incorrect link.
mentions
.into_iter()
.find_map(|mention_link| {
let mut parsed = Url::parse(&mention_link).ok();
let parsed = parsed.as_mut();
parsed.and_then(|gemention_link| {
normalize_url(gemention_link);
if gemention_link == &expected_link {
Some(VerificationStatus::Verified {
endpoint: gemention_link.to_string(),
source: verification_source,
})
} else {
None
}
})
})
.unwrap_or(VerificationStatus::Invalid(
VerificationFailureReason::MentionLinkIncorrect(
mention.receiving_endpoint().to_string(),
),
))
} else {
VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound)
};
Ok(verification_status)
}
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
let target_url = Url::parse(&format!("gemini://{}", mention.target()))?;
let resp = germ_request(&target_url).await?;
let meta = GeminiMetadata::from_string(resp.meta());
let content = resp
.content()
.as_deref()
.ok_or(GementionError::NoContentFoundForTarget)?;
let ast = GeminiAst::from_string(content);
let source_and_endpoints =
scan_for_gemention_endpoints(mention.receiving_endpoint(), meta, ast);
let status = verify_mentions(mention, source_and_endpoints)?;
mention.set_verify_status(status);
Ok(())
}
pub(crate) async fn verify(mention: &mut Mention) -> Result<(), GementionError> {
verify_mention(mention).await
}