use std::str::FromStr;
use dax_core::{
claim::{Claim, ClaimMatchingError, ClaimVerificationError, ClaimVerificationResult},
fetcher::FetchedData,
proof::{find_proof_in_str, Proof, ProofRelation},
serde_json::Value,
service::{Service, ServiceInfo},
};
use dax_fetcher_http::{HttpFetcher, Url};
use regex::Regex;
#[cfg(not(test))]
static REGEX_STR: &str = r"^https:\/\/(.*)\/(.*)\/(.*)\/?";
#[cfg(test)]
static REGEX_STR: &str = r"^http:\/\/(.*)\/(.*)\/(.*)\/?";
pub struct ForgejoService {
info: ServiceInfo,
re: Result<Regex, regex::Error>,
}
impl Service for ForgejoService {
fn new() -> Self {
ForgejoService {
info: ServiceInfo {
name: String::from("Forgejo"),
id: String::from("forgejo"),
homepage: String::from("forgejo"),
},
re: Regex::new(REGEX_STR),
}
}
fn info(&self) -> &ServiceInfo {
&self.info
}
fn match_claim(&self, claim: &Claim) -> Result<bool, ClaimMatchingError> {
let re = self
.re
.as_ref()
.map_err(|_| ClaimMatchingError::ClaimProcessing)?;
Ok(re.is_match(&claim.uri))
}
fn verify_claim(
&self,
claim: &Claim,
proofs: &[Proof],
) -> Result<ClaimVerificationResult, ClaimVerificationError> {
let re = self
.re
.as_ref()
.map_err(|_| ClaimVerificationError::ClaimProcessing)?;
let caps = re
.captures(&claim.uri)
.ok_or(ClaimVerificationError::ServiceMismatch)?;
let domain = caps
.get(1)
.ok_or(ClaimVerificationError::ClaimProcessing)?
.as_str();
let username = caps
.get(2)
.ok_or(ClaimVerificationError::ClaimProcessing)?
.as_str();
let repo = caps
.get(3)
.ok_or(ClaimVerificationError::ClaimProcessing)?
.as_str();
#[cfg(not(test))]
let scheme = "https";
#[cfg(test)]
let scheme = "http";
let url =
Url::from_str(format!("{scheme}://{domain}/api/v1/repos/{username}/{repo}").as_str())
.map_err(|_| ClaimVerificationError::ClaimProcessing)?;
let fetcher = HttpFetcher {};
let result = fetcher.fetch_json(url)?;
let data: Value = if let FetchedData::Json(data) = result {
data
} else {
return Err(ClaimVerificationError::ProofProcessing);
};
let description = data
.get("description")
.ok_or(ClaimVerificationError::ProofProcessing)?;
let description = description
.as_str()
.ok_or(ClaimVerificationError::ProofProcessing)?;
Ok(ClaimVerificationResult {
result: find_proof_in_str(proofs, description, ProofRelation::Contains),
})
}
}
#[cfg(test)]
mod tests {
use dax_core::{
claim::{Claim, ClaimVerificationError, ClaimVerificationResult},
proof::Proof,
serde_json::{json, Value},
service::Service,
};
use httpmock::prelude::*;
use once_cell::sync::Lazy;
use super::ForgejoService;
static API_RESPONSE: Lazy<Value> = Lazy::new(
|| json!({ "description": "My key fingerprint is openpgp4fpr:1234567890123456789012345678901234567890" }),
);
fn run_verification(address: String, proof_str: &str) -> bool {
let claim = Claim::new(&format!("http://{address}/username/repo"));
let proofs = vec![Proof::new(proof_str)];
let service: Box<dyn Service> = Box::new(ForgejoService::new());
let result: Result<ClaimVerificationResult, ClaimVerificationError> =
service.verify_claim(&claim, &proofs);
result.unwrap_or_default().result
}
#[test]
fn match_forgejo_claims() {
let service: Box<dyn Service> = Box::new(ForgejoService::new());
assert!(service
.match_claim(&Claim::new("http://domain.org/alice/forgejo_proof"))
.unwrap());
assert!(service
.match_claim(&Claim::new("http://domain.org/alice/forgejo_proof/"))
.unwrap());
assert!(service
.match_claim(&Claim::new("http://domain.org/alice/other_proof"))
.unwrap());
assert!(!service
.match_claim(&Claim::new("http://domain.org/alice"))
.unwrap());
}
#[test]
fn verify_forgejo_claim() {
let server = MockServer::start();
let m = server.mock(|when, then| {
when.method(GET).path("/api/v1/repos/username/repo");
then.status(200)
.header("content-type", "application/json")
.json_body(API_RESPONSE.clone());
});
let result_valid = run_verification(
server.address().to_string(),
"openpgp4fpr:1234567890123456789012345678901234567890",
);
let result_invalid = run_verification(
server.address().to_string(),
"openpgp4fpr:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
);
assert!(m.hits() > 0);
assert!(result_valid);
assert!(!result_invalid);
}
}