use anyhow::{anyhow, Context, Result};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::{Signer as _, SigningKey};
use parley_core::{
body_sha256_b64url, build_header_value, canonical_query_string, canonical_string, AgentPubkey,
NetworkId, Nonce, SIGNATURE_HEADER,
};
use serde_json::{json, Value};
use crate::state::{Identity, ServerConfig};
pub struct Client {
http: reqwest::Client,
server_url: String,
network: NetworkId,
signing: SigningKey,
pubkey: AgentPubkey,
}
impl Client {
pub fn new(server: &ServerConfig, identity: &Identity) -> Result<Self> {
Ok(Self {
http: reqwest::Client::new(),
server_url: server.server_url.trim_end_matches('/').to_owned(),
network: server.network()?,
signing: identity.signing_key()?,
pubkey: identity.pubkey()?,
})
}
fn sign(&self, method: &str, path: &str, query: &str, body_bytes: &[u8]) -> String {
let nonce = Nonce::generate();
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let body_hash = body_sha256_b64url(body_bytes);
let canonical = canonical_string(
method,
path,
&canonical_query_string(query),
ts,
&nonce,
&self.pubkey,
&self.network,
&body_hash,
);
let sig_bytes = self.signing.sign(canonical.as_bytes()).to_bytes();
build_header_value(&self.pubkey, ts, &nonce, &self.network, &sig_bytes)
}
async fn post_signed(&self, path: &str, body: Value) -> Result<Value> {
let url = format!("{}{}", self.server_url, path);
let body_bytes = serde_json::to_vec(&body).context("serialize body")?;
let header = self.sign("POST", path, "", &body_bytes);
let resp = self
.http
.post(&url)
.header(SIGNATURE_HEADER, header)
.header("content-type", "application/json")
.body(body_bytes)
.send()
.await
.with_context(|| format!("POST {url}"))?;
parse_response(resp).await
}
async fn post_signed_empty(&self, path: &str) -> Result<Value> {
let url = format!("{}{}", self.server_url, path);
let header = self.sign("POST", path, "", b"");
let resp = self
.http
.post(&url)
.header(SIGNATURE_HEADER, header)
.send()
.await
.with_context(|| format!("POST {url}"))?;
parse_response(resp).await
}
async fn get_unsigned(&self, path: &str) -> Result<Value> {
let url = format!("{}{}", self.server_url, path);
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
parse_response(resp).await
}
pub async fn publish_key_packages(&self, packages: Vec<(Vec<u8>, Vec<u8>)>) -> Result<Value> {
let body = json!({
"packages": packages.into_iter().map(|(id, blob)| json!({
"id": URL_SAFE_NO_PAD.encode(id),
"blob": URL_SAFE_NO_PAD.encode(blob),
})).collect::<Vec<_>>()
});
self.post_signed("/v1/agents/me/key_packages", body).await
}
pub async fn claim_key_package(&self, target_pubkey: &AgentPubkey) -> Result<Value> {
let path = format!("/v1/agents/{target_pubkey}/key_packages/claim");
self.post_signed_empty(&path).await
}
pub async fn create_private_channel(
&self,
name: Option<&str>,
group_info: &[u8],
ratchet_tree: &[u8],
welcomes: Vec<(AgentPubkey, Vec<u8>)>,
) -> Result<Value> {
let body = json!({
"name": name,
"group_info": URL_SAFE_NO_PAD.encode(group_info),
"ratchet_tree": URL_SAFE_NO_PAD.encode(ratchet_tree),
"welcomes": welcomes.into_iter().map(|(pk, blob)| json!({
"agent": pk,
"blob": URL_SAFE_NO_PAD.encode(blob),
})).collect::<Vec<_>>(),
});
self.post_signed("/v1/channels/private", body).await
}
pub async fn post_mls_application(&self, channel_id: &str, ciphertext: &[u8]) -> Result<Value> {
let body = json!({
"type": "mls_application",
"content": URL_SAFE_NO_PAD.encode(ciphertext),
});
self.post_signed(&format!("/v1/channels/{channel_id}/messages"), body)
.await
}
pub async fn list_messages(&self, channel_id: &str, limit: u32) -> Result<Value> {
self.get_unsigned(&format!("/v1/channels/{channel_id}/messages?limit={limit}"))
.await
}
pub async fn claim_welcomes(&self) -> Result<Value> {
self.post_signed_empty("/v1/agents/me/welcomes/claim").await
}
pub async fn claim_handle(&self, handle: &str) -> Result<Value> {
self.post_signed("/v1/handles", json!({ "handle": handle }))
.await
}
pub async fn create_blob(
&self,
ciphertext_size: u64,
expires_in_seconds: Option<u64>,
) -> Result<Value> {
let mut body = json!({ "ciphertext_size": ciphertext_size });
if let Some(t) = expires_in_seconds {
body["expires_in_seconds"] = json!(t);
}
self.post_signed("/v1/blobs", body).await
}
pub async fn complete_blob(&self, blob_id: &str) -> Result<Value> {
self.post_signed_empty(&format!("/v1/blobs/{blob_id}/complete"))
.await
}
pub async fn download_blob(&self, blob_id: &str) -> Result<Value> {
let path = format!("/v1/blobs/{blob_id}");
let url = format!("{}{}", self.server_url, path);
let header = self.sign("GET", &path, "", b"");
let resp = self
.http
.get(&url)
.header(SIGNATURE_HEADER, header)
.send()
.await
.with_context(|| format!("GET {url}"))?;
parse_response(resp).await
}
pub async fn put_raw_to_presigned(&self, url: &str, body: Vec<u8>) -> Result<()> {
let resp = self
.http
.put(url)
.header("content-type", "application/octet-stream")
.body(body)
.send()
.await
.with_context(|| format!("PUT presigned {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
anyhow::bail!("S3 PUT returned {status}: {text}");
}
Ok(())
}
pub async fn get_raw_from_presigned(&self, url: &str) -> Result<Vec<u8>> {
let resp = self
.http
.get(url)
.send()
.await
.with_context(|| format!("GET presigned {url}"))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
anyhow::bail!("S3 GET returned {status}: {text}");
}
Ok(resp.bytes().await?.to_vec())
}
pub async fn network_info(&self) -> Result<Value> {
self.get_unsigned("/v1/network/info").await
}
pub async fn register(
&self,
pow_version: u8,
pow_difficulty: u8,
pow_nonce_b64: &str,
) -> Result<Value> {
let body = json!({
"pow": {
"version": pow_version,
"difficulty": pow_difficulty,
"nonce": pow_nonce_b64,
}
});
self.post_signed("/v1/agents/me/register", body).await
}
pub async fn resolve_handle(&self, handle: &str) -> Result<Option<String>> {
let url = format!("{}/v1/handles/{handle}", self.server_url);
let resp = self.http.get(&url).send().await?;
if resp.status().as_u16() == 404 {
return Ok(None);
}
let v: Value = parse_response(resp).await?;
Ok(v.get("pubkey").and_then(|p| p.as_str()).map(str::to_owned))
}
pub async fn handle_for_pubkey(&self, pubkey: &str) -> Result<Option<String>> {
let url = format!("{}/v1/agents/{pubkey}/handle", self.server_url);
let resp = self.http.get(&url).send().await?;
let v: Value = parse_response(resp).await?;
Ok(v.get("handle").and_then(|h| h.as_str()).map(str::to_owned))
}
}
async fn parse_response(resp: reqwest::Response) -> Result<Value> {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
if let Ok(v) = serde_json::from_str::<Value>(&text) {
if let Some(code) = v.get("error").and_then(|c| c.as_str()) {
let msg = v.get("message").and_then(|m| m.as_str()).unwrap_or("");
return Err(anyhow!("server returned {status} {code}: {msg}"));
}
}
return Err(anyhow!("server returned {status}: {text}"));
}
if text.is_empty() {
return Ok(Value::Null);
}
serde_json::from_str(&text).context("parse server response")
}