use std::{fmt::Display, str::FromStr};
use anyhow::{anyhow, Result};
use cid::Cid;
use noosphere_core::{
authority::{generate_capability, SphereAbility, SPHERE_SEMANTICS},
data::{Bundle, Did, Jwt, Link, MemoIpld},
error::NoosphereError,
};
use noosphere_storage::{base64_decode, base64_encode};
use reqwest::StatusCode;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use ucan::{
chain::ProofChain,
crypto::{did::DidParser, KeyMaterial},
store::UcanStore,
Ucan,
};
pub trait AsQuery {
fn as_query(&self) -> Result<Option<String>>;
}
impl AsQuery for () {
fn as_query(&self) -> Result<Option<String>> {
Ok(None)
}
}
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: std::fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => FromStr::from_str(s)
.map_err(serde::de::Error::custom)
.map(Some),
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReplicateParameters {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub since: Option<Link<MemoIpld>>,
}
impl AsQuery for ReplicateParameters {
fn as_query(&self) -> Result<Option<String>> {
Ok(self.since.as_ref().map(|since| format!("since={since}")))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FetchParameters {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub since: Option<Link<MemoIpld>>,
}
impl AsQuery for FetchParameters {
fn as_query(&self) -> Result<Option<String>> {
Ok(self.since.as_ref().map(|since| format!("since={since}")))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum FetchResponse {
NewChanges {
tip: Cid,
},
UpToDate,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PushBody {
pub sphere: Did,
pub local_base: Option<Link<MemoIpld>>,
pub local_tip: Link<MemoIpld>,
pub counterpart_tip: Option<Link<MemoIpld>>,
pub blocks: Bundle,
pub name_record: Option<Jwt>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PushResponse {
Accepted {
new_tip: Link<MemoIpld>,
blocks: Bundle,
},
NoChange,
}
#[derive(Error, Debug)]
pub enum PushError {
#[error("Pushed history conflicts with canonical history")]
Conflict,
#[error("Missing some implied history")]
MissingHistory,
#[error("Replica is up to date")]
UpToDate,
#[error("Internal error")]
Internal(anyhow::Error),
}
impl From<NoosphereError> for PushError {
fn from(error: NoosphereError) -> Self {
error.into()
}
}
impl From<anyhow::Error> for PushError {
fn from(value: anyhow::Error) -> Self {
PushError::Internal(value)
}
}
impl From<PushError> for StatusCode {
fn from(error: PushError) -> Self {
match error {
PushError::Conflict => StatusCode::CONFLICT,
PushError::MissingHistory => StatusCode::UNPROCESSABLE_ENTITY,
PushError::UpToDate => StatusCode::BAD_REQUEST,
PushError::Internal(error) => {
error!("Internal: {:?}", error);
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentifyResponse {
pub gateway_identity: Did,
pub sphere_identity: Did,
pub signature: String,
pub proof: String,
}
impl IdentifyResponse {
pub async fn sign<K>(sphere_identity: &str, key: &K, proof: &Ucan) -> Result<Self>
where
K: KeyMaterial,
{
let gateway_identity = Did(key.get_did().await?);
let signature = base64_encode(
&key.sign(&[gateway_identity.as_bytes(), sphere_identity.as_bytes()].concat())
.await?,
)?;
Ok(IdentifyResponse {
gateway_identity,
sphere_identity: sphere_identity.into(),
signature,
proof: proof.encode()?,
})
}
pub fn shares_identity_with(&self, other: &IdentifyResponse) -> bool {
self.gateway_identity == other.gateway_identity
&& self.sphere_identity == other.sphere_identity
}
pub async fn verify<S: UcanStore>(&self, did_parser: &mut DidParser, store: &S) -> Result<()> {
let gateway_key = did_parser.parse(&self.gateway_identity)?;
let payload_bytes = [
self.gateway_identity.as_bytes(),
self.sphere_identity.as_bytes(),
]
.concat();
let signature_bytes = base64_decode(&self.signature)?;
gateway_key.verify(&payload_bytes, &signature_bytes).await?;
let proof = ProofChain::try_from_token_string(&self.proof, None, did_parser, store).await?;
if proof.ucan().audience() != self.gateway_identity.as_str() {
return Err(anyhow!("Wrong audience!"));
}
let capability = generate_capability(&self.sphere_identity, SphereAbility::Push);
let capability_infos = proof.reduce_capabilities(&SPHERE_SEMANTICS);
for capability_info in capability_infos {
if capability_info.capability.enables(&capability)
&& capability_info
.originators
.contains(self.sphere_identity.as_str())
{
return Ok(());
}
}
Err(anyhow!("Not authorized!"))
}
}
impl Display for IdentifyResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"((Gateway {}), (Sphere {}))",
self.gateway_identity, self.sphere_identity
)
}
}