use std::fmt::Display;
use crate::api::{
data::{empty_string_as_none, AsQuery},
StatusCode,
};
use crate::{
authority::{generate_capability, SphereAbility, SPHERE_SEMANTICS},
data::{Bundle, Did, Jwt, Link, MemoIpld},
error::NoosphereError,
};
use anyhow::{anyhow, Result};
use cid::Cid;
use noosphere_storage::{base64_decode, base64_encode};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use ucan::{
chain::ProofChain,
crypto::{did::DidParser, KeyMaterial},
store::UcanStore,
Ucan,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct ReplicateParameters {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub since: Option<Link<MemoIpld>>,
#[serde(default)]
pub include_content: bool,
}
impl AsQuery for ReplicateParameters {
fn as_query(&self) -> Result<Option<String>> {
let mut params = Vec::new();
if let Some(since) = self.since {
params.push(format!("since={since}"));
}
if self.include_content {
params.push(String::from("include_content=true"))
}
let query = if !params.is_empty() {
Some(params.join("&"))
} else {
None
};
Ok(query)
}
}
#[derive(Clone)]
pub enum ReplicationMode {
Cid(Cid),
Did(Did),
}
impl From<Cid> for ReplicationMode {
fn from(value: Cid) -> Self {
ReplicationMode::Cid(value)
}
}
impl From<Did> for ReplicationMode {
fn from(value: Did) -> Self {
ReplicationMode::Did(value)
}
}
impl Display for ReplicationMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReplicationMode::Cid(cid) => Display::fmt(cid, f),
ReplicationMode::Did(did) => Display::fmt(did, f),
}
}
}
#[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 {
#[allow(missing_docs)]
#[error("Pushed history conflicts with canonical history")]
Conflict,
#[allow(missing_docs)]
#[error("Missing some implied history")]
MissingHistory,
#[allow(missing_docs)]
#[error("Replica is up to date")]
UpToDate,
#[allow(missing_docs)]
#[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
)
}
}