use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{mpsc, Arc, Mutex, RwLock};
use std::time::{Duration, SystemTime};
#[cfg(feature = "native")]
use std::sync::Condvar;
#[cfg(feature = "native")]
use std::time::Instant;
use bottlers::{Bottle, IDCard, Keychain, Opener, PrivateKey};
use spotproto::{Message, MSG_FLAG_ERROR, MSG_FLAG_NOT_BOTTLE, MSG_FLAG_RESPONSE};
use crate::error::{Error, Result};
use crate::events::{ClientEvent, Hub};
use crate::identity;
use crate::utils::{uuid_string, uuid_v4};
#[cfg(feature = "native")]
use crate::conn;
#[cfg(not(feature = "native"))]
use crate::conn_wasm;
#[cfg(not(feature = "native"))]
use futures_channel::oneshot;
pub type MessageHandler =
Arc<dyn Fn(&Message) -> std::result::Result<Option<Vec<u8>>, String> + Send + Sync>;
const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) struct IdState {
pub card: IDCard,
pub signed: Vec<u8>,
}
#[cfg(feature = "native")]
#[derive(Default)]
pub(crate) struct WriteQueue {
q: Mutex<VecDeque<Message>>,
cv: Condvar,
}
#[cfg(feature = "native")]
impl WriteQueue {
pub fn push(&self, msg: Message) {
self.q.lock().unwrap().push_back(msg);
self.cv.notify_one();
}
pub fn push_front(&self, msg: Message) {
self.q.lock().unwrap().push_front(msg);
self.cv.notify_one();
}
pub fn pop_timeout(&self, dur: Duration) -> Option<Message> {
let mut q = self.q.lock().unwrap();
let deadline = Instant::now() + dur;
loop {
if let Some(msg) = q.pop_front() {
return Some(msg);
}
let now = Instant::now();
if now >= deadline {
return None;
}
let (guard, _) = self.cv.wait_timeout(q, deadline - now).unwrap();
q = guard;
}
}
pub fn wake_all(&self) {
self.cv.notify_all();
}
}
pub(crate) struct Inner {
pub kc: Keychain,
pub signer_pkix: Vec<u8>,
pub opener: Opener,
pub id: Mutex<IdState>,
pub events: Hub,
pub hosts: Mutex<HashSet<String>>,
pub min_conn: AtomicU32,
pub conn_cnt: AtomicU32,
pub online_cnt: Mutex<u32>,
pub handlers: RwLock<HashMap<String, MessageHandler>>,
pub id_cache: Mutex<HashMap<Vec<u8>, Arc<IDCard>>>,
pub closed: AtomicBool,
#[cfg(feature = "native")]
pub wrq: WriteQueue,
#[cfg(feature = "native")]
pub online_cv: Condvar,
#[cfg(feature = "native")]
pub in_q: Mutex<HashMap<String, mpsc::Sender<Message>>>,
#[cfg(not(feature = "native"))]
pub sink: Mutex<Option<rsurl::aio::WsSink>>,
#[cfg(not(feature = "native"))]
pub outq: Mutex<VecDeque<Message>>,
#[cfg(not(feature = "native"))]
pub online_waiters: Mutex<Vec<oneshot::Sender<()>>>,
#[cfg(not(feature = "native"))]
pub in_q: Mutex<HashMap<String, oneshot::Sender<Message>>>,
}
impl Inner {
pub fn signer(&self) -> &PrivateKey {
self.kc
.get_key(&self.signer_pkix)
.expect("signer key is always in the keychain")
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::Relaxed)
}
pub fn logf(&self, args: std::fmt::Arguments<'_>) {
if std::env::var_os("SPOTLIB_DEBUG").is_some() {
eprintln!("spot client: {args}");
}
}
pub fn id_bin(&self) -> Vec<u8> {
self.id.lock().unwrap().signed.clone()
}
pub fn handle_groups(&self, groups: &[Vec<u8>]) -> Result<()> {
let mut st = self.id.lock().unwrap();
identity::update_groups(&mut st.card, groups)?;
st.signed = st.card.sign(self.signer())?;
Ok(())
}
pub fn get_handler(&self, endpoint: &str) -> Option<MessageHandler> {
self.handlers.read().unwrap().get(endpoint).cloned()
}
pub fn prepare_message(&self, rid: Option<&IDCard>, payload: &[u8]) -> Result<Vec<u8>> {
let mut bottle = Bottle::new(payload.to_vec());
if let Some(rid) = rid {
let keys = rid.keys_for("decrypt", identity::now_unix());
bottle.encrypt(&keys)?;
bottle.bottle_up()?;
}
bottle.sign(self.signer())?;
Ok(bottle.to_cbor()?)
}
pub fn decode_message(&self, rid: Option<&IDCard>, payload: &[u8]) -> Result<Vec<u8>> {
let (buf, info) = self.opener.open_cbor(payload)?;
if let Some(rid) = rid {
if info.decryption == 0 {
return Err(Error::Other("incoming message is not encrypted".into()));
}
if !identity::signed_by(&info, rid) {
self.need_key_refresh();
return Err(Error::Other(
"incoming message is not signed by sender".into(),
));
}
}
Ok(buf)
}
fn get_idcard_from_cache(&self, h: &[u8]) -> Option<Arc<IDCard>> {
self.id_cache.lock().unwrap().get(h).cloned()
}
pub fn set_idcard_cache(&self, h: Vec<u8>, card: IDCard) {
let mut cache = self.id_cache.lock().unwrap();
if cache.len() > 1024 {
cache.clear();
}
cache.insert(h, Arc::new(card));
}
fn need_key_refresh(&self) {
self.id_cache.lock().unwrap().clear();
}
pub fn target_id(&self) -> String {
let st = self.id.lock().unwrap();
let h = bottlers::hash::sha256(&st.card.self_key);
format!("k.{}", spotproto::base64url_encode(&h))
}
fn recipient_hash(rcv: &str) -> Result<Vec<u8>> {
let rcv = rcv.split('/').next().unwrap_or(rcv);
let parts: Vec<&str> = rcv.split('.').collect();
if parts.len() < 2 || parts[0] != "k" {
return Err(Error::InvalidTarget(rcv.to_string()));
}
spotproto::base64url_decode(parts[parts.len() - 1])
.ok_or_else(|| Error::InvalidTarget(rcv.to_string()))
}
}
#[cfg(feature = "native")]
impl Inner {
pub fn online_incr(&self) {
let mut cnt = self.online_cnt.lock().unwrap();
*cnt += 1;
if *cnt == 1 {
self.events.emit(ClientEvent::Status(
*cnt,
self.conn_cnt.load(Ordering::Relaxed),
));
self.events.emit(ClientEvent::Online);
}
self.online_cv.notify_all();
}
pub fn online_decr(&self) {
let mut cnt = self.online_cnt.lock().unwrap();
*cnt -= 1;
if *cnt == 0 {
self.events.emit(ClientEvent::Status(
*cnt,
self.conn_cnt.load(Ordering::Relaxed),
));
self.events.emit(ClientEvent::Offline);
}
}
pub fn make_in_q(&self, key: String) -> mpsc::Receiver<Message> {
let (tx, rx) = mpsc::channel();
self.in_q.lock().unwrap().insert(key, tx);
rx
}
pub fn take_in_q(&self, key: &str) -> Option<mpsc::Sender<Message>> {
self.in_q.lock().unwrap().remove(key)
}
pub fn route_message(self: &Arc<Self>, msg: Message) {
let rcv = &msg.recipient;
let Some(pos) = rcv.find('/') else { return };
let mut name = &rcv[pos + 1..];
if let Some(pos2) = name.find('/') {
name = &name[..pos2];
}
if let Some(q) = self.take_in_q(name) {
let _ = q.send(msg);
} else if let Some(h) = self.get_handler(name) {
let inner = self.clone();
std::thread::spawn(move || inner.run_handler(msg, h));
} else {
self.logf(format_args!(
"unable to route packet targetted to {}",
msg.recipient
));
}
}
fn run_handler(self: Arc<Self>, mut msg: Message, h: MessageHandler) {
let mut rid: Option<Arc<IDCard>> = None;
if msg.flags & MSG_FLAG_NOT_BOTTLE == 0 {
let deadline = Instant::now() + DEFAULT_QUERY_TIMEOUT;
match self.get_idcard_for_recipient(&msg.sender, deadline) {
Ok(card) => rid = Some(card),
Err(e) => {
self.logf(format_args!("cannot send encrypted response: {e}"));
return;
}
}
match self.decode_message(rid.as_deref(), &msg.body) {
Ok(body) => msg.body = body,
Err(e) => {
self.logf(format_args!("failed to decode incoming message: {e}"));
return;
}
}
}
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h(&msg)))
.unwrap_or_else(|e| {
let text = e
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".into());
Err(format!("panic in handler: {text}"))
});
if msg.flags & MSG_FLAG_RESPONSE == MSG_FLAG_RESPONSE {
return;
}
let mut res_flags = MSG_FLAG_RESPONSE;
let body = match res {
Ok(None) => return, Ok(Some(body)) => body,
Err(e) => {
res_flags |= MSG_FLAG_ERROR;
e.into_bytes()
}
};
let body = if msg.flags & MSG_FLAG_NOT_BOTTLE == 0 {
match self.prepare_message(rid.as_deref(), &body) {
Ok(b) => b,
Err(e) => {
self.logf(format_args!("failed to prepare response: {e}"));
return;
}
}
} else {
res_flags |= MSG_FLAG_NOT_BOTTLE;
body
};
self.wrq.push(Message {
message_id: msg.message_id,
flags: res_flags,
recipient: msg.sender.clone(),
sender: "/noreply".into(),
body,
});
}
pub fn query(
self: &Arc<Self>,
target: &str,
body: &[u8],
deadline: Instant,
) -> Result<Vec<u8>> {
if target.is_empty() {
return Err(Error::InvalidTarget(String::new()));
}
let mut rid: Option<Arc<IDCard>> = None;
if target.starts_with('k') {
rid = Some(self.get_idcard_for_recipient(target, deadline)?);
}
let body = self.prepare_message(rid.as_deref(), body)?;
let id = uuid_v4();
let id_str = uuid_string(&id);
let rx = self.make_in_q(id_str.clone());
let _guard = InQGuard {
inner: self,
key: &id_str,
};
self.wrq.push(Message {
message_id: id,
flags: 0,
recipient: target.to_string(),
sender: format!("/{id_str}"),
body,
});
let now = Instant::now();
if now >= deadline {
return Err(Error::Timeout);
}
let mut obj = match rx.recv_timeout(deadline - now) {
Ok(msg) => msg,
Err(_) => {
return if self.is_closed() {
Err(Error::Closed)
} else {
Err(Error::Timeout)
}
}
};
if obj.flags & MSG_FLAG_NOT_BOTTLE == 0 {
obj.body = self
.decode_message(rid.as_deref(), &obj.body)
.map_err(|e| Error::Other(format!("failed to decode response: {e}")))?;
} else if rid.is_some() {
return Err(Error::Other(
"remote failed to respond with an encrypted response".into(),
));
}
if obj.flags & MSG_FLAG_ERROR != 0 {
return Err(Error::Remote(
String::from_utf8_lossy(&obj.body).into_owned(),
));
}
Ok(obj.body)
}
pub fn get_idcard(self: &Arc<Self>, h: &[u8], deadline: Instant) -> Result<Arc<IDCard>> {
if let Some(card) = self.get_idcard_from_cache(h) {
return Ok(card);
}
let buf = self.query("@/idcard_find", h, deadline)?;
let card = IDCard::from_signed(&buf)?;
let card = Arc::new(card);
self.id_cache
.lock()
.unwrap()
.insert(h.to_vec(), card.clone());
Ok(card)
}
pub fn send_to_with_from(
self: &Arc<Self>,
target: &str,
payload: &[u8],
from: &str,
deadline: Instant,
) -> Result<()> {
let rid = self.get_idcard_for_recipient(target, deadline)?;
let body = self.prepare_message(Some(&rid), payload)?;
let id = uuid_v4();
let from = if from.is_empty() {
format!("/{}", uuid_string(&id))
} else {
if !from.starts_with('/') {
return Err(Error::InvalidTarget(from.to_string()));
}
from.to_string()
};
self.wrq.push(Message {
message_id: id,
flags: 0,
recipient: target.to_string(),
sender: from,
body,
});
Ok(())
}
pub fn get_idcard_for_recipient(
self: &Arc<Self>,
rcv: &str,
deadline: Instant,
) -> Result<Arc<IDCard>> {
let h = Self::recipient_hash(rcv)?;
self.get_idcard(&h, deadline)
}
}
#[cfg(feature = "native")]
struct InQGuard<'a> {
inner: &'a Inner,
key: &'a str,
}
#[cfg(feature = "native")]
impl Drop for InQGuard<'_> {
fn drop(&mut self) {
self.inner.take_in_q(self.key);
}
}
#[cfg(not(feature = "native"))]
impl Inner {
pub fn online_incr(&self) {
let mut cnt = self.online_cnt.lock().unwrap();
*cnt += 1;
if *cnt == 1 {
self.events.emit(ClientEvent::Status(
*cnt,
self.conn_cnt.load(Ordering::Relaxed),
));
self.events.emit(ClientEvent::Online);
}
drop(cnt);
self.wake_online_waiters();
}
pub fn online_decr(&self) {
let mut cnt = self.online_cnt.lock().unwrap();
*cnt -= 1;
if *cnt == 0 {
self.events.emit(ClientEvent::Status(
*cnt,
self.conn_cnt.load(Ordering::Relaxed),
));
self.events.emit(ClientEvent::Offline);
}
drop(cnt);
self.wake_online_waiters();
}
pub fn wake_online_waiters(&self) {
let waiters = std::mem::take(&mut *self.online_waiters.lock().unwrap());
for tx in waiters {
let _ = tx.send(());
}
}
pub fn set_sink(&self, sink: rsurl::aio::WsSink) {
*self.sink.lock().unwrap() = Some(sink);
self.flush_out();
}
pub fn drop_sink(&self) {
*self.sink.lock().unwrap() = None;
}
pub fn send_raw(&self, buf: &[u8]) -> Result<()> {
match self.sink.lock().unwrap().as_ref() {
Some(s) => s.send_binary(buf).map_err(|e| Error::Ws(e.to_string())),
None => Err(Error::Ws("no active connection".into())),
}
}
pub fn push_out(&self, msg: Message) {
self.outq.lock().unwrap().push_back(msg);
self.flush_out();
}
fn flush_out(&self) {
let mut sink = self.sink.lock().unwrap();
if sink.is_none() {
return;
}
loop {
let Some(msg) = self.outq.lock().unwrap().pop_front() else {
break;
};
let buf = match spotproto::Packet::Message(msg.clone()).encode() {
Ok(buf) => buf,
Err(e) => {
self.logf(format_args!("failed to encode message: {e}"));
continue;
}
};
let sent = sink
.as_ref()
.map(|s| s.send_binary(&buf).is_ok())
.unwrap_or(false);
if !sent {
self.outq.lock().unwrap().push_front(msg);
*sink = None;
break;
}
}
}
pub fn make_in_q(&self, key: String) -> oneshot::Receiver<Message> {
let (tx, rx) = oneshot::channel();
self.in_q.lock().unwrap().insert(key, tx);
rx
}
pub fn take_in_q(&self, key: &str) -> Option<oneshot::Sender<Message>> {
self.in_q.lock().unwrap().remove(key)
}
pub fn route_message(self: &Arc<Self>, msg: Message) {
let rcv = &msg.recipient;
let Some(pos) = rcv.find('/') else { return };
let mut name = &rcv[pos + 1..];
if let Some(pos2) = name.find('/') {
name = &name[..pos2];
}
if let Some(q) = self.take_in_q(name) {
let _ = q.send(msg);
} else if let Some(h) = self.get_handler(name) {
let inner = self.clone();
wasm_bindgen_futures::spawn_local(async move { inner.run_handler(msg, h).await });
} else {
self.logf(format_args!(
"unable to route packet targetted to {}",
msg.recipient
));
}
}
async fn run_handler(self: Arc<Self>, mut msg: Message, h: MessageHandler) {
let mut rid: Option<Arc<IDCard>> = None;
if msg.flags & MSG_FLAG_NOT_BOTTLE == 0 {
match self
.get_idcard_for_recipient(&msg.sender, DEFAULT_QUERY_TIMEOUT)
.await
{
Ok(card) => rid = Some(card),
Err(e) => {
self.logf(format_args!("cannot send encrypted response: {e}"));
return;
}
}
match self.decode_message(rid.as_deref(), &msg.body) {
Ok(body) => msg.body = body,
Err(e) => {
self.logf(format_args!("failed to decode incoming message: {e}"));
return;
}
}
}
let res = h(&msg);
if msg.flags & MSG_FLAG_RESPONSE == MSG_FLAG_RESPONSE {
return;
}
let mut res_flags = MSG_FLAG_RESPONSE;
let body = match res {
Ok(None) => return, Ok(Some(body)) => body,
Err(e) => {
res_flags |= MSG_FLAG_ERROR;
e.into_bytes()
}
};
let body = if msg.flags & MSG_FLAG_NOT_BOTTLE == 0 {
match self.prepare_message(rid.as_deref(), &body) {
Ok(b) => b,
Err(e) => {
self.logf(format_args!("failed to prepare response: {e}"));
return;
}
}
} else {
res_flags |= MSG_FLAG_NOT_BOTTLE;
body
};
self.push_out(Message {
message_id: msg.message_id,
flags: res_flags,
recipient: msg.sender.clone(),
sender: "/noreply".into(),
body,
});
}
pub async fn query(
self: &Arc<Self>,
target: &str,
body: &[u8],
timeout: Duration,
) -> Result<Vec<u8>> {
if target.is_empty() {
return Err(Error::InvalidTarget(String::new()));
}
let mut rid: Option<Arc<IDCard>> = None;
if target.starts_with('k') {
rid = Some(self.get_idcard_for_recipient(target, timeout).await?);
}
let body = self.prepare_message(rid.as_deref(), body)?;
let id = uuid_v4();
let id_str = uuid_string(&id);
let rx = self.make_in_q(id_str.clone());
let _guard = InQGuard {
inner: self,
key: &id_str,
};
self.push_out(Message {
message_id: id,
flags: 0,
recipient: target.to_string(),
sender: format!("/{id_str}"),
body,
});
let mut obj = match conn_wasm::with_timeout(rx, timeout).await {
Some(Ok(msg)) => msg,
Some(Err(_)) => return Err(Error::Closed),
None => {
return if self.is_closed() {
Err(Error::Closed)
} else {
Err(Error::Timeout)
}
}
};
if obj.flags & MSG_FLAG_NOT_BOTTLE == 0 {
obj.body = self
.decode_message(rid.as_deref(), &obj.body)
.map_err(|e| Error::Other(format!("failed to decode response: {e}")))?;
} else if rid.is_some() {
return Err(Error::Other(
"remote failed to respond with an encrypted response".into(),
));
}
if obj.flags & MSG_FLAG_ERROR != 0 {
return Err(Error::Remote(
String::from_utf8_lossy(&obj.body).into_owned(),
));
}
Ok(obj.body)
}
pub async fn get_idcard(self: &Arc<Self>, h: &[u8], timeout: Duration) -> Result<Arc<IDCard>> {
if let Some(card) = self.get_idcard_from_cache(h) {
return Ok(card);
}
let buf = Box::pin(self.query("@/idcard_find", h, timeout)).await?;
let card = IDCard::from_signed(&buf)?;
let card = Arc::new(card);
self.id_cache
.lock()
.unwrap()
.insert(h.to_vec(), card.clone());
Ok(card)
}
pub async fn send_to_with_from(
self: &Arc<Self>,
target: &str,
payload: &[u8],
from: &str,
timeout: Duration,
) -> Result<()> {
let rid = self.get_idcard_for_recipient(target, timeout).await?;
let body = self.prepare_message(Some(&rid), payload)?;
let id = uuid_v4();
let from = if from.is_empty() {
format!("/{}", uuid_string(&id))
} else {
if !from.starts_with('/') {
return Err(Error::InvalidTarget(from.to_string()));
}
from.to_string()
};
self.push_out(Message {
message_id: id,
flags: 0,
recipient: target.to_string(),
sender: from,
body,
});
Ok(())
}
pub async fn get_idcard_for_recipient(
self: &Arc<Self>,
rcv: &str,
timeout: Duration,
) -> Result<Arc<IDCard>> {
let h = Self::recipient_hash(rcv)?;
self.get_idcard(&h, timeout).await
}
}
#[cfg(not(feature = "native"))]
struct InQGuard<'a> {
inner: &'a Inner,
key: &'a str,
}
#[cfg(not(feature = "native"))]
impl Drop for InQGuard<'_> {
fn drop(&mut self) {
self.inner.take_in_q(self.key);
}
}
#[derive(Default)]
pub struct ClientBuilder {
keys: Vec<PrivateKey>,
meta: BTreeMap<String, String>,
handlers: HashMap<String, MessageHandler>,
}
impl ClientBuilder {
pub fn key(mut self, key: PrivateKey) -> Self {
self.keys.push(key);
self
}
pub fn keychain(mut self, kc: Keychain) -> Self {
for key in kc.keys() {
if let Ok(copy) = identity::clone_private_key(key) {
self.keys.push(copy);
}
}
self
}
pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.meta.insert(key.into(), value.into());
self
}
pub fn handler<F>(mut self, endpoint: impl Into<String>, h: F) -> Self
where
F: Fn(&Message) -> std::result::Result<Option<Vec<u8>>, String> + Send + Sync + 'static,
{
self.handlers.insert(endpoint.into(), Arc::new(h));
self
}
pub fn build(self) -> Result<Client> {
let mut kc = Keychain::new();
let mut opener_kc = Keychain::new();
let ephemeral = self.keys.is_empty();
let mut keys = self.keys;
if ephemeral {
let sk = purecrypto::ec::ecdsa::EcdsaPrivateKey::generate(&mut purecrypto::rng::OsRng);
keys.push(PrivateKey::Ecdsa(sk));
}
for key in keys {
opener_kc
.add_key(identity::clone_private_key(&key)?)
.map_err(Error::Bottle)?;
kc.add_key(key).map_err(Error::Bottle)?;
}
let signer_pkix = kc
.first_signer()
.ok_or_else(|| Error::Other("no signing key available".into()))?
.public_pkix()?;
let signer = kc.get_key(&signer_pkix).unwrap();
let mut card = IDCard::new(signer, identity::now_unix())?;
card.meta = Some(self.meta);
identity::add_keychain(&mut card, &kc)?;
if ephemeral {
identity::add_key_purposes(
&mut card,
signer_pkix.clone(),
&["ephemeral"],
identity::now_unix(),
);
}
let signed = card.sign(signer)?;
let mut handlers = self.handlers;
default_handlers(&mut handlers);
let inner = Arc::new(Inner {
kc,
signer_pkix,
opener: Opener::new(opener_kc),
id: Mutex::new(IdState { card, signed }),
events: Hub::new(),
hosts: Mutex::new(HashSet::new()),
min_conn: AtomicU32::new(1),
conn_cnt: AtomicU32::new(0),
online_cnt: Mutex::new(0),
handlers: RwLock::new(handlers),
id_cache: Mutex::new(HashMap::new()),
closed: AtomicBool::new(false),
#[cfg(feature = "native")]
wrq: WriteQueue::default(),
#[cfg(feature = "native")]
online_cv: Condvar::new(),
#[cfg(feature = "native")]
in_q: Mutex::new(HashMap::new()),
#[cfg(not(feature = "native"))]
sink: Mutex::new(None),
#[cfg(not(feature = "native"))]
outq: Mutex::new(VecDeque::new()),
#[cfg(not(feature = "native"))]
online_waiters: Mutex::new(Vec::new()),
#[cfg(not(feature = "native"))]
in_q: Mutex::new(HashMap::new()),
});
register_inner_handlers(&inner);
#[cfg(feature = "native")]
{
let main_inner = inner.clone();
std::thread::spawn(move || conn::main_thread(main_inner));
}
#[cfg(not(feature = "native"))]
{
let main_inner = inner.clone();
wasm_bindgen_futures::spawn_local(
async move { conn_wasm::main_loop(main_inner).await },
);
}
Ok(Client { inner })
}
}
fn default_handlers(handlers: &mut HashMap<String, MessageHandler>) {
handlers.entry("ping".to_string()).or_insert_with(|| {
Arc::new(|msg: &Message| {
let body = if msg.body.len() > 128 {
msg.body[..128].to_vec()
} else {
msg.body.clone()
};
Ok(Some(body))
})
});
handlers.entry("version".to_string()).or_insert_with(|| {
Arc::new(|_: &Message| {
Ok(Some(
format!("spotlib-rs/{}", env!("CARGO_PKG_VERSION")).into_bytes(),
))
})
});
}
fn register_inner_handlers(inner: &Arc<Inner>) {
let mut handlers = inner.handlers.write().unwrap();
let finger_inner = Arc::downgrade(inner);
handlers.entry("finger".to_string()).or_insert_with(|| {
Arc::new(move |_: &Message| match finger_inner.upgrade() {
Some(inner) => Ok(Some(inner.id_bin())),
None => Err("client is closed".into()),
})
});
let idcard_inner = Arc::downgrade(inner);
handlers
.entry("idcard_update".to_string())
.or_insert_with(|| {
Arc::new(move |msg: &Message| {
if msg.body.is_empty() {
return Err("empty ID card data received".into());
}
let idc = IDCard::from_signed(&msg.body)
.map_err(|e| format!("invalid ID card format: {e}"))?;
if let Some(inner) = idcard_inner.upgrade() {
let h = bottlers::hash::sha256(&idc.self_key);
inner.set_idcard_cache(h.to_vec(), idc);
}
Ok(None)
})
});
}
pub struct Client {
inner: Arc<Inner>,
}
impl Client {
pub(crate) fn inner(&self) -> &Arc<Inner> {
&self.inner
}
pub fn new() -> Result<Client> {
ClientBuilder::default().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn close(&self) {
self.inner.closed.store(true, Ordering::Relaxed);
#[cfg(feature = "native")]
{
self.inner.wrq.wake_all();
self.inner.online_cv.notify_all();
}
#[cfg(not(feature = "native"))]
{
self.inner.wake_online_waiters();
}
}
pub fn id_card(&self) -> IDCard {
self.inner.id.lock().unwrap().card.clone()
}
pub fn id_card_bin(&self) -> Vec<u8> {
self.inner.id_bin()
}
pub fn target_id(&self) -> String {
self.inner.target_id()
}
pub fn connection_count(&self) -> (u32, u32) {
(
self.inner.conn_cnt.load(Ordering::Relaxed),
*self.inner.online_cnt.lock().unwrap(),
)
}
pub fn subscribe_events(&self) -> mpsc::Receiver<ClientEvent> {
self.inner.events.subscribe()
}
pub fn set_handler<F>(&self, endpoint: impl Into<String>, handler: Option<F>)
where
F: Fn(&Message) -> std::result::Result<Option<Vec<u8>>, String> + Send + Sync + 'static,
{
let mut handlers = self.inner.handlers.write().unwrap();
match handler {
Some(h) => {
handlers.insert(endpoint.into(), Arc::new(h));
}
None => {
handlers.remove(&endpoint.into());
}
}
}
}
#[cfg(feature = "native")]
impl Client {
pub fn wait_online(&self, timeout: Duration) -> Result<()> {
let deadline = Instant::now() + timeout;
let mut cnt = self.inner.online_cnt.lock().unwrap();
loop {
let want = self.inner.min_conn.load(Ordering::Relaxed).max(1);
if *cnt >= want {
return Ok(());
}
if self.inner.is_closed() {
return Err(Error::Closed);
}
let now = Instant::now();
if now >= deadline {
return Err(Error::Timeout);
}
let (guard, _) = self
.inner
.online_cv
.wait_timeout(cnt, deadline - now)
.unwrap();
cnt = guard;
}
}
pub fn query(&self, target: &str, body: &[u8], timeout: Duration) -> Result<Vec<u8>> {
self.inner.query(target, body, Instant::now() + timeout)
}
pub fn send_to(&self, target: &str, payload: &[u8], timeout: Duration) -> Result<()> {
self.send_to_with_from(target, payload, "", timeout)
}
pub fn send_to_with_from(
&self,
target: &str,
payload: &[u8],
from: &str,
timeout: Duration,
) -> Result<()> {
self.inner
.send_to_with_from(target, payload, from, Instant::now() + timeout)
}
pub fn get_group_members(&self, group_key: &[u8], timeout: Duration) -> Result<Vec<String>> {
let buf = self.query("@/group_list", group_key, timeout)?;
Ok(buf
.chunks(32)
.map(|h| format!("k.{}", spotproto::base64url_encode(h)))
.collect())
}
pub fn store_blob(&self, key: &str, value: &[u8], timeout: Duration) -> Result<()> {
if value.is_empty() {
self.query("@/store_blob", format!("{key}\0").as_bytes(), timeout)?;
return Ok(());
}
let body = self.inner.build_store_blob(key, value)?;
self.query("@/store_blob", &body, timeout)?;
Ok(())
}
pub fn fetch_blob(&self, key: &str, timeout: Duration) -> Result<Vec<u8>> {
let buf = self.query("@/fetch_blob", key.as_bytes(), timeout)?;
self.inner.open_fetched_blob(&buf)
}
pub fn get_idcard_bin(&self, h: &[u8], timeout: Duration) -> Result<Vec<u8>> {
self.query("@/idcard_find", h, timeout)
}
pub fn get_idcard(&self, h: &[u8], timeout: Duration) -> Result<Arc<IDCard>> {
self.inner.get_idcard(h, Instant::now() + timeout)
}
pub fn get_idcard_for_recipient(&self, rcv: &str, timeout: Duration) -> Result<Arc<IDCard>> {
self.inner
.get_idcard_for_recipient(rcv, Instant::now() + timeout)
}
pub fn get_time(&self, timeout: Duration) -> Result<SystemTime> {
let res = self.query("@/time", &[], timeout)?;
parse_server_time(&res)
}
}
#[cfg(not(feature = "native"))]
impl Client {
pub async fn wait_online(&self, timeout: Duration) -> Result<()> {
let deadline_ms = conn_wasm::now_ms() + timeout.as_millis() as f64;
loop {
{
let cnt = self.inner.online_cnt.lock().unwrap();
let want = self.inner.min_conn.load(Ordering::Relaxed).max(1);
if *cnt >= want {
return Ok(());
}
}
if self.inner.is_closed() {
return Err(Error::Closed);
}
let remaining = deadline_ms - conn_wasm::now_ms();
if remaining <= 0.0 {
return Err(Error::Timeout);
}
let (tx, rx) = oneshot::channel();
self.inner.online_waiters.lock().unwrap().push(tx);
let _ = conn_wasm::with_timeout(rx, Duration::from_millis(remaining as u64)).await;
}
}
pub async fn query(&self, target: &str, body: &[u8], timeout: Duration) -> Result<Vec<u8>> {
self.inner.query(target, body, timeout).await
}
pub async fn send_to(&self, target: &str, payload: &[u8], timeout: Duration) -> Result<()> {
self.send_to_with_from(target, payload, "", timeout).await
}
pub async fn send_to_with_from(
&self,
target: &str,
payload: &[u8],
from: &str,
timeout: Duration,
) -> Result<()> {
self.inner
.send_to_with_from(target, payload, from, timeout)
.await
}
pub async fn get_group_members(
&self,
group_key: &[u8],
timeout: Duration,
) -> Result<Vec<String>> {
let buf = self.query("@/group_list", group_key, timeout).await?;
Ok(buf
.chunks(32)
.map(|h| format!("k.{}", spotproto::base64url_encode(h)))
.collect())
}
pub async fn store_blob(&self, key: &str, value: &[u8], timeout: Duration) -> Result<()> {
if value.is_empty() {
self.query("@/store_blob", format!("{key}\0").as_bytes(), timeout)
.await?;
return Ok(());
}
let body = self.inner.build_store_blob(key, value)?;
self.query("@/store_blob", &body, timeout).await?;
Ok(())
}
pub async fn fetch_blob(&self, key: &str, timeout: Duration) -> Result<Vec<u8>> {
let buf = self.query("@/fetch_blob", key.as_bytes(), timeout).await?;
self.inner.open_fetched_blob(&buf)
}
pub async fn get_idcard_bin(&self, h: &[u8], timeout: Duration) -> Result<Vec<u8>> {
self.query("@/idcard_find", h, timeout).await
}
pub async fn get_idcard(&self, h: &[u8], timeout: Duration) -> Result<Arc<IDCard>> {
self.inner.get_idcard(h, timeout).await
}
pub async fn get_idcard_for_recipient(
&self,
rcv: &str,
timeout: Duration,
) -> Result<Arc<IDCard>> {
self.inner.get_idcard_for_recipient(rcv, timeout).await
}
pub async fn get_time(&self, timeout: Duration) -> Result<SystemTime> {
let res = self.query("@/time", &[], timeout).await?;
parse_server_time(&res)
}
}
impl Inner {
fn build_store_blob(&self, key: &str, value: &[u8]) -> Result<Vec<u8>> {
let mut bottle = Bottle::new(value.to_vec());
{
let st = self.id.lock().unwrap();
let keys = st.card.keys_for("decrypt", identity::now_unix());
bottle.encrypt(&keys)?;
}
bottle.bottle_up()?;
let mut sig_cnt = 0;
let mut sig_err: Option<bottlers::BottleError> = None;
for key in self.kc.keys() {
match bottle.sign(key) {
Ok(()) => sig_cnt += 1,
Err(e) => sig_err = Some(e),
}
}
if sig_cnt == 0 {
return Err(match sig_err {
Some(e) => Error::Bottle(e),
None => Error::Other("no signature key was available".into()),
});
}
let buf = bottle.to_cbor()?;
let mut body = format!("{key}\0").into_bytes();
body.extend_from_slice(&buf);
Ok(body)
}
fn open_fetched_blob(&self, buf: &[u8]) -> Result<Vec<u8>> {
let (data, info) = self.opener.open_cbor(buf)?;
let signed_ok = {
let st = self.id.lock().unwrap();
identity::signed_by(&info, &st.card)
};
if !signed_ok {
return Err(Error::Other("data was not signed by us".into()));
}
if info.decryption == 0 {
return Err(Error::Other("data was not encrypted".into()));
}
Ok(data)
}
}
fn parse_server_time(res: &[u8]) -> Result<SystemTime> {
if res.len() < 12 {
return Err(Error::Other("unable to parse time from server".into()));
}
let secs = u64::from_be_bytes(res[..8].try_into().unwrap());
let nanos = u32::from_be_bytes(res[8..12].try_into().unwrap());
Ok(SystemTime::UNIX_EPOCH + Duration::new(secs, nanos))
}
impl Drop for Client {
fn drop(&mut self) {
self.close();
}
}