1use std::{
2 collections::BTreeMap,
3 str::FromStr,
4 sync::{Arc, RwLock},
5};
6
7use anyhow::{Result, anyhow, ensure};
8use iroh::{Endpoint, EndpointAddr, EndpointId, endpoint::ConnectError};
9use iroh_metrics::{MetricsGroup, Registry, encoding::Encoder};
10use irpc_iroh::IrohLazyRemoteConnection;
11use n0_error::StackResultExt;
12use n0_future::{task::AbortOnDropHandle, time::Duration};
13use rcan::Rcan;
14use tokio::sync::oneshot;
15use tracing::{debug, trace, warn};
16use uuid::Uuid;
17
18use crate::{
19 api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
20 caps::{Caps, DEFAULT_CAP_EXPIRY},
21 net_diagnostics::{DiagnosticsReport, checks::run_diagnostics},
22 protocol::{
23 ALPN, Auth, IrohServicesClient, NameEndpoint, Ping, Pong, PutMetrics,
24 PutNetworkDiagnostics, RemoteError, SetAttributes, SetGroup,
25 },
26};
27
28#[derive(Debug, Clone)]
52pub struct Client {
53 #[allow(dead_code)]
55 endpoint: Endpoint,
56 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
57 _actor_task: Arc<AbortOnDropHandle<()>>,
58}
59
60pub struct ClientBuilder {
63 #[allow(dead_code)]
64 cap_expiry: Duration,
65 cap: Option<Rcan<Caps>>,
66 endpoint: Endpoint,
67 name: Option<String>,
68 group: Option<String>,
69 attributes: Option<BTreeMap<String, String>>,
70 metrics_interval: Option<Duration>,
71 remote: Option<EndpointAddr>,
72 registry: Registry,
73}
74
75impl ClientBuilder {
76 pub fn new(endpoint: &Endpoint) -> Self {
77 let mut registry = Registry::default();
78 registry.register_all(endpoint.metrics());
79
80 Self {
81 cap: None,
82 cap_expiry: DEFAULT_CAP_EXPIRY,
83 endpoint: endpoint.clone(),
84 name: None,
85 group: None,
86 attributes: None,
87 metrics_interval: Some(Duration::from_secs(60)),
88 remote: None,
89 registry,
90 }
91 }
92
93 pub fn register_metrics_group(mut self, metrics_group: Arc<dyn MetricsGroup>) -> Self {
97 self.registry.register(metrics_group);
98 self
99 }
100
101 pub fn metrics_interval(mut self, interval: Duration) -> Self {
105 self.metrics_interval = Some(interval);
106 self
107 }
108
109 pub fn disable_metrics_interval(mut self) -> Self {
111 self.metrics_interval = None;
112 self
113 }
114
115 pub fn name(mut self, name: impl Into<String>) -> Result<Self> {
127 let name = name.into();
128 validate_name(&name).map_err(BuildError::InvalidName)?;
129 self.name = Some(name);
130 Ok(self)
131 }
132
133 pub fn group(mut self, group: impl Into<String>) -> Result<Self> {
142 let group = group.into();
143 validate_name(&group).map_err(BuildError::InvalidGroup)?;
144 self.group = Some(group);
145 Ok(self)
146 }
147
148 pub fn attributes<I, K, V>(mut self, attrs: I) -> Result<Self>
166 where
167 I: IntoIterator<Item = (K, V)>,
168 K: Into<String>,
169 V: Into<String>,
170 {
171 let collected: BTreeMap<String, String> = attrs
172 .into_iter()
173 .map(|(k, v)| (k.into(), v.into()))
174 .collect();
175 validate_attributes(&collected).map_err(BuildError::InvalidAttributes)?;
176 self.attributes = Some(collected);
177 Ok(self)
178 }
179
180 pub fn api_secret_from_env(self) -> Result<Self> {
182 let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
183 self.api_secret(ticket)
184 }
185
186 pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
188 let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
189 self.api_secret(key)
190 }
191
192 pub fn api_secret(mut self, ticket: ApiSecret) -> Result<Self> {
199 let local_id = self.endpoint.id();
200 let rcan = crate::caps::create_api_token_from_secret_key(
201 ticket.secret,
202 local_id,
203 self.cap_expiry,
204 Caps::for_shared_secret(),
205 )?;
206
207 self.remote = Some(ticket.remote);
208 self.rcan(rcan)
209 }
210
211 #[cfg(not(wasm_browser))]
215 pub async fn ssh_key_from_file<P: AsRef<std::path::Path>>(self, path: P) -> Result<Self> {
216 let file_content = tokio::fs::read_to_string(path).await?;
217 self.ssh_key(&file_content)
218 }
219
220 #[cfg(not(wasm_browser))]
222 pub fn ssh_key(mut self, pem: &str) -> Result<Self> {
223 let local_id = self.endpoint.id();
224 let rcan = crate::caps::create_api_token_from_openssh_pem(
225 pem,
226 local_id,
227 self.cap_expiry,
228 Caps::all(),
229 )?;
230 self.cap.replace(rcan);
231
232 Ok(self)
233 }
234
235 pub fn rcan(mut self, cap: Rcan<Caps>) -> Result<Self> {
237 ensure!(
238 EndpointId::from_verifying_key(*cap.audience()) == self.endpoint.id(),
239 "invalid audience"
240 );
241 self.cap.replace(cap);
242 Ok(self)
243 }
244
245 pub fn remote(mut self, remote: impl Into<EndpointAddr>) -> Self {
248 self.remote = Some(remote.into());
249 self
250 }
251
252 #[must_use = "dropping the client will silently cancel all client tasks"]
254 pub async fn build(self) -> Result<Client, BuildError> {
255 debug!("starting iroh-services client");
256 let remote = self.remote.ok_or(BuildError::MissingRemote)?;
257 let capabilities = self.cap.ok_or(BuildError::MissingCapability)?;
258
259 let conn = IrohLazyRemoteConnection::new(self.endpoint.clone(), remote, ALPN.to_vec());
260 let irpc_client = IrohServicesClient::boxed(conn);
261
262 let (tx, rx) = tokio::sync::mpsc::channel(1);
263 let actor_task = AbortOnDropHandle::new(n0_future::task::spawn(
264 ClientActor {
265 capabilities,
266 client: irpc_client,
267 name: self.name.clone(),
268 group: self.group.clone(),
269 attributes: self.attributes.clone().unwrap_or_default(),
270 session_id: Uuid::new_v4(),
271 authorized: false,
272 }
273 .run(self.registry, self.metrics_interval, rx),
274 ));
275
276 Ok(Client {
277 endpoint: self.endpoint,
278 message_channel: tx,
279 _actor_task: Arc::new(actor_task),
280 })
281 }
282}
283
284#[derive(thiserror::Error, Debug)]
285pub enum BuildError {
286 #[error("Missing remote endpoint to dial")]
287 MissingRemote,
288 #[error("Missing capability")]
289 MissingCapability,
290 #[error("Unauthorized")]
291 Unauthorized,
292 #[error("Remote error: {0}")]
293 Remote(#[from] RemoteError),
294 #[error("Rpc connection error: {0}")]
295 Rpc(irpc::Error),
296 #[error("Connection error: {0}")]
297 Connect(ConnectError),
298 #[error("Invalid endpoint name: {0}")]
299 InvalidName(#[from] ValidateNameError),
300 #[error("Invalid endpoint group: {0}")]
301 InvalidGroup(ValidateNameError),
302 #[error("Invalid endpoint attributes: {0}")]
303 InvalidAttributes(#[from] ValidateAttributesError),
304}
305
306impl From<irpc::Error> for BuildError {
307 fn from(value: irpc::Error) -> Self {
308 match value {
309 irpc::Error::Request {
310 source:
311 irpc::RequestError::Connection {
312 source: iroh::endpoint::ConnectionError::ApplicationClosed(frame),
313 ..
314 },
315 ..
316 } if frame.error_code == 401u32.into() => Self::Unauthorized,
317 value => Self::Rpc(value),
318 }
319 }
320}
321
322pub const CLIENT_NAME_MIN_LENGTH: usize = 2;
324pub const CLIENT_NAME_MAX_LENGTH: usize = 128;
326
327#[derive(Debug, thiserror::Error)]
329pub enum ValidateNameError {
330 #[error("Name is too long (must be no more than {CLIENT_NAME_MAX_LENGTH} bytes).")]
331 TooLong,
332 #[error("Name is too short (must be at least {CLIENT_NAME_MIN_LENGTH} bytes).")]
333 TooShort,
334}
335
336fn validate_name(name: &str) -> Result<(), ValidateNameError> {
337 if name.len() < CLIENT_NAME_MIN_LENGTH {
338 Err(ValidateNameError::TooShort)
339 } else if name.len() > CLIENT_NAME_MAX_LENGTH {
340 Err(ValidateNameError::TooLong)
341 } else {
342 Ok(())
343 }
344}
345
346pub const CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH: usize = 128;
348pub const CLIENT_ATTRIBUTES_MAX_COUNT: usize = 128;
350
351#[derive(Debug, thiserror::Error)]
353pub enum ValidateAttributesError {
354 #[error("Too many attributes (must be no more than {CLIENT_ATTRIBUTES_MAX_COUNT}).")]
355 TooManyEntries,
356 #[error("Invalid attribute key: {0}")]
357 InvalidKey(#[from] ValidateNameError),
358 #[error(
359 "Attribute value too long (must be no more than {CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH} bytes)."
360 )]
361 ValueTooLong,
362}
363
364fn validate_attributes(attrs: &BTreeMap<String, String>) -> Result<(), ValidateAttributesError> {
365 if attrs.len() > CLIENT_ATTRIBUTES_MAX_COUNT {
366 return Err(ValidateAttributesError::TooManyEntries);
367 }
368 for (k, v) in attrs {
369 validate_name(k)?;
370 if v.len() > CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH {
371 return Err(ValidateAttributesError::ValueTooLong);
372 }
373 }
374 Ok(())
375}
376
377#[derive(thiserror::Error, Debug)]
378pub enum Error {
379 #[error("Invalid endpoint name: {0}")]
380 InvalidName(#[from] ValidateNameError),
381 #[error("Invalid endpoint group: {0}")]
382 InvalidGroup(ValidateNameError),
383 #[error("Invalid endpoint attributes: {0}")]
384 InvalidAttributes(#[from] ValidateAttributesError),
385 #[error("Remote error: {0}")]
386 Remote(#[from] RemoteError),
387 #[error("Connection error: {0}")]
388 Rpc(#[from] irpc::Error),
389 #[error(transparent)]
390 Other(#[from] anyhow::Error),
391}
392
393impl Client {
394 pub fn builder(endpoint: &Endpoint) -> ClientBuilder {
395 ClientBuilder::new(endpoint)
396 }
397
398 pub async fn name(&self) -> Result<Option<String>, Error> {
400 let (tx, rx) = oneshot::channel();
401 self.message_channel
402 .send(ClientActorMessage::ReadName { done: tx })
403 .await
404 .map_err(|_| Error::Other(anyhow!("sending name read request")))?;
405
406 rx.await
407 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))
408 }
409
410 pub async fn group(&self) -> Result<Option<String>, Error> {
412 let (tx, rx) = oneshot::channel();
413 self.message_channel
414 .send(ClientActorMessage::ReadGroup { done: tx })
415 .await
416 .map_err(|_| Error::Other(anyhow!("sending group read request")))?;
417
418 rx.await
419 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))
420 }
421
422 pub async fn set_name(&self, name: impl Into<String>) -> Result<(), Error> {
427 set_name_inner(self.message_channel.clone(), name.into()).await
428 }
429
430 pub async fn set_group(&self, group: impl Into<String>) -> Result<(), Error> {
434 set_group_inner(self.message_channel.clone(), group.into()).await
435 }
436
437 pub async fn set_attributes<I, K, V>(&self, attrs: I) -> Result<(), Error>
455 where
456 I: IntoIterator<Item = (K, V)>,
457 K: Into<String>,
458 V: Into<String>,
459 {
460 let collected: BTreeMap<String, String> = attrs
461 .into_iter()
462 .map(|(k, v)| (k.into(), v.into()))
463 .collect();
464 set_attributes_inner(self.message_channel.clone(), collected).await
465 }
466
467 pub async fn set_attribute(
474 &self,
475 key: impl Into<String>,
476 value: impl Into<String>,
477 ) -> Result<(), Error> {
478 set_attribute_inner(self.message_channel.clone(), key.into(), value.into()).await
479 }
480
481 pub async fn ping(&self) -> Result<Pong, Error> {
483 let (tx, rx) = oneshot::channel();
484 self.message_channel
485 .send(ClientActorMessage::Ping { done: tx })
486 .await
487 .map_err(|_| Error::Other(anyhow!("sending ping request")))?;
488
489 rx.await
490 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
491 .map_err(Error::Remote)
492 }
493
494 pub async fn push_metrics(&self) -> Result<(), Error> {
498 let (tx, rx) = oneshot::channel();
499 self.message_channel
500 .send(ClientActorMessage::SendMetrics { done: tx })
501 .await
502 .map_err(|_| Error::Other(anyhow!("sending metrics")))?;
503
504 rx.await
505 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
506 .map_err(Error::Remote)
507 }
508
509 pub async fn grant_capability(
513 &self,
514 remote_id: EndpointId,
515 caps: impl IntoIterator<Item = impl Into<crate::caps::Cap>>,
516 ) -> Result<(), Error> {
517 let cap = crate::caps::create_grant_token(
518 self.endpoint.secret_key().clone(),
519 remote_id,
520 DEFAULT_CAP_EXPIRY,
521 Caps::new(caps),
522 )
523 .map_err(Error::Other)?;
524
525 let (tx, rx) = oneshot::channel();
526 self.message_channel
527 .send(ClientActorMessage::GrantCap {
528 cap: Box::new(cap),
529 done: tx,
530 })
531 .await
532 .map_err(|_| Error::Other(anyhow!("granting capability")))?;
533
534 rx.await
535 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
536 }
537
538 pub async fn net_diagnostics(&self, send: bool) -> Result<DiagnosticsReport, Error> {
540 let report = run_diagnostics(&self.endpoint).await?;
541 if send {
542 let (tx, rx) = oneshot::channel();
543 self.message_channel
544 .send(ClientActorMessage::PutNetworkDiagnostics {
545 done: tx,
546 report: Box::new(report.clone()),
547 })
548 .await
549 .map_err(|_| Error::Other(anyhow!("sending network diagnostics report")))?;
550
551 let _ = rx
552 .await
553 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?;
554 }
555
556 Ok(report)
557 }
558}
559
560enum ClientActorMessage {
561 SendMetrics {
562 done: oneshot::Sender<Result<(), RemoteError>>,
563 },
564 Ping {
565 done: oneshot::Sender<Result<Pong, RemoteError>>,
566 },
567 #[allow(dead_code)]
569 GrantCap {
570 cap: Box<Rcan<Caps>>,
572 done: oneshot::Sender<Result<(), Error>>,
573 },
574 PutNetworkDiagnostics {
575 report: Box<DiagnosticsReport>,
576 done: oneshot::Sender<Result<(), Error>>,
577 },
578 ReadName {
579 done: oneshot::Sender<Option<String>>,
580 },
581 ReadGroup {
582 done: oneshot::Sender<Option<String>>,
583 },
584 NameEndpoint {
585 name: String,
586 done: oneshot::Sender<Result<(), RemoteError>>,
587 },
588 SetGroup {
589 group: String,
590 done: oneshot::Sender<Result<(), RemoteError>>,
591 },
592 SetAttributes {
593 attributes: BTreeMap<String, String>,
594 done: oneshot::Sender<Result<(), RemoteError>>,
595 },
596 SetAttribute {
597 key: String,
598 value: String,
599 done: oneshot::Sender<Result<(), Error>>,
603 },
604}
605
606struct ClientActor {
607 capabilities: Rcan<Caps>,
608 client: IrohServicesClient,
609 name: Option<String>,
610 group: Option<String>,
611 attributes: BTreeMap<String, String>,
612 session_id: Uuid,
613 authorized: bool,
614}
615
616impl ClientActor {
617 async fn run(
618 mut self,
619 registry: Registry,
620 interval: Option<Duration>,
621 mut inbox: tokio::sync::mpsc::Receiver<ClientActorMessage>,
622 ) {
623 let registry = Arc::new(RwLock::new(registry));
624 let mut encoder = Encoder::new(registry);
625 let mut metrics_timer = interval.map(|interval| n0_future::time::interval(interval));
626 trace!("starting client actor");
627
628 if let Some(name) = self.name.clone()
631 && let Err(err) = self.send_name_endpoint(name).await
632 {
633 warn!(err = %err, "failed setting endpoint name on startup");
634 }
635
636 if let Some(group) = self.group.clone()
637 && let Err(err) = self.send_set_group(group).await
638 {
639 warn!(err = %err, "failed setting endpoint group on startup");
640 }
641
642 if !self.attributes.is_empty()
643 && let Err(err) = self.send_set_attributes(self.attributes.clone()).await
644 {
645 warn!(err = %err, "failed setting endpoint attributes on startup");
646 }
647
648 loop {
649 trace!("client actor tick");
650 tokio::select! {
651 biased;
652 Some(msg) = inbox.recv() => {
653 match msg {
654 ClientActorMessage::Ping{ done } => {
655 let res = self.send_ping().await;
656 if let Err(err) = done.send(res) {
657 debug!("failed to send ping: {:#?}", err);
658 self.authorized = false;
659 }
660 },
661 ClientActorMessage::SendMetrics{ done } => {
662 trace!("sending metrics manually triggered");
663 let res = self.send_metrics(&mut encoder).await;
664 if let Err(err) = done.send(res) {
665 debug!("failed to push metrics: {:#?}", err);
666 self.authorized = false;
667 }
668 }
669 ClientActorMessage::GrantCap{ cap, done } => {
670 let res = self.grant_cap(*cap).await;
671 if let Err(err) = done.send(res) {
672 warn!("failed to grant capability: {:#?}", err);
673 }
674 }
675 ClientActorMessage::ReadName{ done } => {
676 if let Err(err) = done.send(self.name.clone()) {
677 warn!("sending name value: {:#?}", err);
678 }
679 }
680 ClientActorMessage::ReadGroup{ done } => {
681 if let Err(err) = done.send(self.group.clone()) {
682 warn!("sending group value: {:#?}", err);
683 }
684 }
685 ClientActorMessage::NameEndpoint{ name, done } => {
686 let res = self.send_name_endpoint(name).await;
687 if let Err(err) = done.send(res) {
688 warn!("failed to name endpoint: {:#?}", err);
689 }
690 }
691 ClientActorMessage::SetGroup{ group, done } => {
692 let res = self.send_set_group(group).await;
693 if let Err(err) = done.send(res) {
694 warn!("failed to set group: {:#?}", err);
695 }
696 }
697 ClientActorMessage::SetAttributes{ attributes, done } => {
698 let res = self.send_set_attributes(attributes).await;
699 if let Err(err) = done.send(res) {
700 warn!("failed to set attributes: {:#?}", err);
701 }
702 }
703 ClientActorMessage::SetAttribute{ key, value, done } => {
704 let mut merged = self.attributes.clone();
709 merged.insert(key, value);
710 let res = match validate_attributes(&merged) {
711 Ok(()) => {
712 self.send_set_attributes(merged).await.map_err(Error::Remote)
713 }
714 Err(err) => Err(Error::from(err)),
715 };
716 if let Err(err) = done.send(res) {
717 warn!("failed to set attribute: {:#?}", err);
718 }
719 }
720 ClientActorMessage::PutNetworkDiagnostics{ report, done } => {
721 let res = self.put_network_diagnostics(*report).await;
722 if let Err(err) = done.send(res) {
723 warn!("failed to publish network diagnostics: {:#?}", err);
724 }
725 }
726 }
727 }
728 _ = async {
729 if let Some(ref mut timer) = metrics_timer {
730 timer.tick().await;
731 } else {
732 std::future::pending::<()>().await;
733 }
734 } => {
735 trace!("metrics send tick");
736 if let Err(err) = self.send_metrics(&mut encoder).await {
737 debug!("failed to push metrics: {:#?}", err);
738 self.authorized = false;
739 }
740 },
741 }
742 }
743 }
744
745 async fn auth(&mut self) -> Result<(), RemoteError> {
747 if self.authorized {
748 return Ok(());
749 }
750 trace!("client authorizing");
751 self.client
752 .rpc(Auth {
753 caps: self.capabilities.clone(),
754 })
755 .await
756 .inspect_err(|e| debug!("authorization failed: {:?}", e))
757 .map_err(|e| RemoteError::AuthError(e.to_string()))?;
758 self.authorized = true;
759 Ok(())
760 }
761
762 async fn send_ping(&mut self) -> Result<Pong, RemoteError> {
763 trace!("client actor send ping");
764 self.auth().await?;
765
766 let req = rand::random();
767 self.client
768 .rpc(Ping { req_id: req })
769 .await
770 .inspect_err(|e| warn!("rpc ping error: {e}"))
771 .map_err(|_| RemoteError::InternalServerError)
772 }
773
774 async fn send_name_endpoint(&mut self, name: String) -> Result<(), RemoteError> {
775 trace!("client sending name endpoint request");
776 self.auth().await?;
777
778 self.client
779 .rpc(NameEndpoint { name: name.clone() })
780 .await
781 .inspect_err(|e| debug!("name endpoint error: {e}"))
782 .map_err(|_| RemoteError::InternalServerError)??;
783 self.name = Some(name);
784 Ok(())
785 }
786
787 async fn send_set_group(&mut self, group: String) -> Result<(), RemoteError> {
788 trace!("client sending set group request");
789 self.auth().await?;
790
791 self.client
792 .rpc(SetGroup {
793 group: group.clone(),
794 })
795 .await
796 .inspect_err(|e| debug!("set group error: {e}"))
797 .map_err(|_| RemoteError::InternalServerError)??;
798 self.group = Some(group);
799 Ok(())
800 }
801
802 async fn send_set_attributes(
803 &mut self,
804 attributes: BTreeMap<String, String>,
805 ) -> Result<(), RemoteError> {
806 trace!("client sending set attributes request");
807 self.auth().await?;
808
809 self.client
810 .rpc(SetAttributes {
811 attributes: attributes.clone(),
812 })
813 .await
814 .inspect_err(|e| debug!("set attributes error: {e}"))
815 .map_err(|_| RemoteError::InternalServerError)??;
816 self.attributes = attributes;
817 Ok(())
818 }
819
820 async fn send_metrics(&mut self, encoder: &mut Encoder) -> Result<(), RemoteError> {
821 trace!("client actor send metrics");
822 self.auth().await?;
823
824 let update = encoder.export();
825 let req = PutMetrics {
827 session_id: self.session_id,
828 update,
829 };
830
831 self.client
832 .rpc(req)
833 .await
834 .map_err(|_| RemoteError::InternalServerError)??;
835
836 Ok(())
837 }
838
839 async fn grant_cap(&mut self, cap: Rcan<Caps>) -> Result<(), Error> {
840 trace!("client actor grant capability");
841 self.auth().await?;
842
843 self.client
844 .rpc(crate::protocol::GrantCap { cap })
845 .await
846 .map_err(|_| RemoteError::InternalServerError)??;
847
848 Ok(())
849 }
850
851 async fn put_network_diagnostics(
852 &mut self,
853 report: crate::net_diagnostics::DiagnosticsReport,
854 ) -> Result<(), Error> {
855 trace!("client actor publish network diagnostics");
856 self.auth().await?;
857
858 let req = PutNetworkDiagnostics { report };
859
860 self.client
861 .rpc(req)
862 .await
863 .map_err(|_| RemoteError::InternalServerError)??;
864
865 Ok(())
866 }
867}
868
869async fn set_name_inner(
870 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
871 name: String,
872) -> Result<(), Error> {
873 validate_name(&name)?;
874 debug!(name_len = name.len(), "calling set name");
875 let (tx, rx) = oneshot::channel();
876 message_channel
877 .send(ClientActorMessage::NameEndpoint { name, done: tx })
878 .await
879 .map_err(|_| Error::Other(anyhow!("sending name endpoint request")))?;
880 rx.await
881 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
882 .map_err(Error::Remote)
883}
884
885async fn set_group_inner(
886 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
887 group: String,
888) -> Result<(), Error> {
889 validate_name(&group).map_err(Error::InvalidGroup)?;
890 debug!(%group, "calling set group");
891 let (tx, rx) = oneshot::channel();
892 message_channel
893 .send(ClientActorMessage::SetGroup { group, done: tx })
894 .await
895 .map_err(|_| Error::Other(anyhow!("sending set group request")))?;
896 rx.await
897 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
898 .map_err(Error::Remote)
899}
900
901async fn set_attributes_inner(
902 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
903 attributes: BTreeMap<String, String>,
904) -> Result<(), Error> {
905 validate_attributes(&attributes)?;
906 debug!(attr_count = attributes.len(), "calling set attributes");
907 let (tx, rx) = oneshot::channel();
908 message_channel
909 .send(ClientActorMessage::SetAttributes {
910 attributes,
911 done: tx,
912 })
913 .await
914 .map_err(|_| Error::Other(anyhow!("sending set attributes request")))?;
915 rx.await
916 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
917 .map_err(Error::Remote)
918}
919
920async fn set_attribute_inner(
921 message_channel: tokio::sync::mpsc::Sender<ClientActorMessage>,
922 key: String,
923 value: String,
924) -> Result<(), Error> {
925 let (tx, rx) = oneshot::channel();
929 message_channel
930 .send(ClientActorMessage::SetAttribute {
931 key,
932 value,
933 done: tx,
934 })
935 .await
936 .map_err(|_| Error::Other(anyhow!("sending set attribute request")))?;
937 rx.await
938 .map_err(|e| Error::Other(anyhow!("response on internal channel: {:?}", e)))?
939}
940
941#[cfg(test)]
942mod tests {
943 use std::collections::HashMap;
944
945 use iroh::{Endpoint, EndpointAddr, SecretKey, endpoint::presets};
946 use rand::{RngExt, SeedableRng};
947 use temp_env_vars::temp_env_vars;
948
949 use crate::{
950 Client,
951 api_secret::ApiSecret,
952 caps::{Cap, Caps},
953 client::{
954 API_SECRET_ENV_VAR_NAME, BuildError, CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH,
955 CLIENT_ATTRIBUTES_MAX_COUNT, CLIENT_NAME_MAX_LENGTH, Error, ValidateAttributesError,
956 ValidateNameError,
957 },
958 };
959
960 #[tokio::test]
961 #[temp_env_vars]
962 async fn test_api_key_from_env() {
963 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
965 let shared_secret = SecretKey::from_bytes(&rng.random());
966 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
967 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
968 unsafe {
969 std::env::set_var(API_SECRET_ENV_VAR_NAME, api_secret.to_string());
970 };
971
972 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
973
974 let builder = Client::builder(&endpoint).api_secret_from_env().unwrap();
975
976 let fake_endpoint_addr: EndpointAddr = fake_endpoint_id.into();
977 assert_eq!(builder.remote, Some(fake_endpoint_addr));
978
979 let cap = builder.cap.as_ref().expect("expected capability to be set");
982 assert_eq!(cap.capability(), &Caps::new([Cap::Client]));
983 assert_eq!(cap.audience(), &endpoint.id().as_verifying_key());
984 assert_eq!(cap.issuer(), &shared_secret.public().as_verifying_key());
985 }
986
987 #[tokio::test]
990 async fn test_no_metrics_interval() {
991 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(1);
992 let shared_secret = SecretKey::from_bytes(&rng.random());
993 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
994 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
995
996 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
997
998 let client = Client::builder(&endpoint)
999 .disable_metrics_interval()
1000 .api_secret(api_secret)
1001 .unwrap()
1002 .build()
1003 .await
1004 .unwrap();
1005
1006 let err = client.push_metrics().await;
1007 assert!(err.is_err());
1008 }
1009
1010 #[tokio::test]
1011 async fn test_name() {
1012 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1013 let shared_secret = SecretKey::from_bytes(&rng.random());
1014 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1015 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1016
1017 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1018
1019 let builder = Client::builder(&endpoint)
1020 .name("my-node 👋")
1021 .unwrap()
1022 .api_secret(api_secret)
1023 .unwrap();
1024
1025 assert_eq!(builder.name, Some("my-node 👋".to_string()));
1026
1027 let Err(err) = Client::builder(&endpoint).name("a") else {
1028 panic!("name should fail for strings under 2 bytes");
1029 };
1030 assert!(matches!(
1031 err.downcast_ref::<BuildError>(),
1032 Some(BuildError::InvalidName(ValidateNameError::TooShort))
1033 ));
1034
1035 let too_long_name = "👋".repeat(129);
1036 let Err(err) = Client::builder(&endpoint).name(&too_long_name) else {
1037 panic!("name should fail for strings over 128 bytes");
1038 };
1039 assert!(matches!(
1040 err.downcast_ref::<BuildError>(),
1041 Some(BuildError::InvalidName(ValidateNameError::TooLong))
1042 ));
1043 }
1044
1045 #[tokio::test]
1046 async fn test_group() {
1047 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(0);
1048 let shared_secret = SecretKey::from_bytes(&rng.random());
1049 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1050 let api_secret = ApiSecret::new(shared_secret.clone(), fake_endpoint_id);
1051
1052 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1053
1054 let builder = Client::builder(&endpoint)
1055 .group("staging")
1056 .unwrap()
1057 .api_secret(api_secret)
1058 .unwrap();
1059
1060 assert_eq!(builder.group, Some("staging".to_string()));
1061
1062 let Err(err) = Client::builder(&endpoint).group("a") else {
1063 panic!("group should fail for strings under 2 bytes");
1064 };
1065 assert!(matches!(
1066 err.downcast_ref::<BuildError>(),
1067 Some(BuildError::InvalidGroup(ValidateNameError::TooShort))
1068 ));
1069
1070 let too_long_group = "👋".repeat(129);
1071 let Err(err) = Client::builder(&endpoint).group(&too_long_group) else {
1072 panic!("group should fail for strings over 128 bytes");
1073 };
1074 assert!(matches!(
1075 err.downcast_ref::<BuildError>(),
1076 Some(BuildError::InvalidGroup(ValidateNameError::TooLong))
1077 ));
1078 }
1079
1080 #[tokio::test]
1081 async fn test_attributes() {
1082 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1083
1084 let builder = Client::builder(&endpoint)
1086 .attributes(std::iter::empty::<(String, String)>())
1087 .unwrap();
1088 assert_eq!(builder.attributes.as_ref().map(|m| m.len()), Some(0));
1089
1090 let builder = Client::builder(&endpoint)
1092 .attributes([("env", "prod"), ("region", "us-west")])
1093 .unwrap();
1094 let attrs = builder.attributes.as_ref().expect("attributes set");
1095 assert_eq!(attrs.get("env").map(String::as_str), Some("prod"));
1096 assert_eq!(attrs.get("region").map(String::as_str), Some("us-west"));
1097
1098 let mut map: HashMap<String, String> = HashMap::new();
1100 map.insert("k1".into(), "v1".into());
1101 map.insert("k2".into(), "".into()); let builder = Client::builder(&endpoint).attributes(map).unwrap();
1103 let attrs = builder.attributes.as_ref().expect("attributes set");
1104 assert_eq!(attrs.get("k2").map(String::as_str), Some(""));
1105
1106 let too_long_value = "x".repeat(129);
1108 let Err(err) = Client::builder(&endpoint).attributes([("ok", too_long_value.as_str())])
1109 else {
1110 panic!("attributes should fail for value over 128 bytes");
1111 };
1112 assert!(matches!(
1113 err.downcast_ref::<BuildError>(),
1114 Some(BuildError::InvalidAttributes(
1115 ValidateAttributesError::ValueTooLong
1116 ))
1117 ));
1118
1119 let Err(err) = Client::builder(&endpoint).attributes([("a", "v")]) else {
1121 panic!("attributes should fail for key under 2 bytes");
1122 };
1123 assert!(matches!(
1124 err.downcast_ref::<BuildError>(),
1125 Some(BuildError::InvalidAttributes(
1126 ValidateAttributesError::InvalidKey(ValidateNameError::TooShort)
1127 ))
1128 ));
1129
1130 let big: Vec<(String, String)> = (0..(CLIENT_ATTRIBUTES_MAX_COUNT + 1))
1132 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1133 .collect();
1134 let Err(err) = Client::builder(&endpoint).attributes(big) else {
1135 panic!("attributes should fail for more than 128 entries");
1136 };
1137 assert!(matches!(
1138 err.downcast_ref::<BuildError>(),
1139 Some(BuildError::InvalidAttributes(
1140 ValidateAttributesError::TooManyEntries
1141 ))
1142 ));
1143 }
1144
1145 async fn build_serverless_client(seed: u64) -> Client {
1149 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(seed);
1150 let shared_secret = SecretKey::from_bytes(&rng.random());
1151 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1152 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1153
1154 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1155
1156 Client::builder(&endpoint)
1157 .disable_metrics_interval()
1158 .api_secret(api_secret)
1159 .unwrap()
1160 .build()
1161 .await
1162 .unwrap()
1163 }
1164
1165 #[tokio::test]
1168 async fn test_set_group_runtime_validation() {
1169 let client = build_serverless_client(2).await;
1170
1171 let err = client
1172 .set_group("a")
1173 .await
1174 .expect_err("too-short group should fail validation");
1175 assert!(matches!(
1176 err,
1177 Error::InvalidGroup(ValidateNameError::TooShort)
1178 ));
1179
1180 let too_long = "x".repeat(CLIENT_NAME_MAX_LENGTH + 1);
1181 let err = client
1182 .set_group(too_long)
1183 .await
1184 .expect_err("too-long group should fail validation");
1185 assert!(matches!(
1186 err,
1187 Error::InvalidGroup(ValidateNameError::TooLong)
1188 ));
1189 }
1190
1191 #[tokio::test]
1194 async fn test_set_attributes_runtime_validation() {
1195 let client = build_serverless_client(3).await;
1196
1197 let err = client
1199 .set_attributes([("a", "v")])
1200 .await
1201 .expect_err("too-short attribute key should fail validation");
1202 assert!(matches!(
1203 err,
1204 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1205 ValidateNameError::TooShort
1206 ))
1207 ));
1208
1209 let too_long_value = "x".repeat(CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH + 1);
1211 let err = client
1212 .set_attributes([("ok", too_long_value.as_str())])
1213 .await
1214 .expect_err("too-long attribute value should fail validation");
1215 assert!(matches!(
1216 err,
1217 Error::InvalidAttributes(ValidateAttributesError::ValueTooLong)
1218 ));
1219
1220 let big: Vec<(String, String)> = (0..(CLIENT_ATTRIBUTES_MAX_COUNT + 1))
1222 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1223 .collect();
1224 let err = client
1225 .set_attributes(big)
1226 .await
1227 .expect_err("too many attributes should fail validation");
1228 assert!(matches!(
1229 err,
1230 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1231 ));
1232 }
1233
1234 #[tokio::test]
1235 async fn test_set_attribute_runtime_validation() {
1236 let client = build_serverless_client(7).await;
1237
1238 let err = client
1240 .set_attribute("a", "v")
1241 .await
1242 .expect_err("too-short attribute key should fail validation");
1243 assert!(matches!(
1244 err,
1245 Error::InvalidAttributes(ValidateAttributesError::InvalidKey(
1246 ValidateNameError::TooShort
1247 ))
1248 ));
1249
1250 let err = client
1254 .set_attribute("firmware", "2.1.0")
1255 .await
1256 .expect_err("no server: remote call must fail after validation passes");
1257 assert!(matches!(err, Error::Remote(_)), "got {err:?}");
1258 }
1259
1260 #[tokio::test]
1261 async fn test_set_attribute_merge_over_limit_rejected() {
1262 let full: Vec<(String, String)> = (0..CLIENT_ATTRIBUTES_MAX_COUNT)
1264 .map(|i| (format!("key_{i:04}"), "v".to_string()))
1265 .collect();
1266
1267 let mut rng = rand::rngs::ChaCha8Rng::seed_from_u64(9);
1268 let shared_secret = SecretKey::from_bytes(&rng.random());
1269 let fake_endpoint_id = SecretKey::from_bytes(&rng.random()).public();
1270 let api_secret = ApiSecret::new(shared_secret, fake_endpoint_id);
1271 let endpoint = Endpoint::builder(presets::Minimal).bind().await.unwrap();
1272 let client = Client::builder(&endpoint)
1273 .disable_metrics_interval()
1274 .attributes(full)
1275 .unwrap()
1276 .api_secret(api_secret)
1277 .unwrap()
1278 .build()
1279 .await
1280 .unwrap();
1281
1282 let err = client
1286 .set_attribute("one-too-many", "v")
1287 .await
1288 .expect_err("merging past the attribute limit must fail");
1289 assert!(
1290 matches!(
1291 err,
1292 Error::InvalidAttributes(ValidateAttributesError::TooManyEntries)
1293 ),
1294 "expected TooManyEntries, got {err:?}"
1295 );
1296 }
1297
1298 #[tokio::test]
1303 async fn test_set_attributes_runtime_boundary_accepted() {
1304 let client = build_serverless_client(4).await;
1305
1306 let max_value = "x".repeat(CLIENT_ATTRIBUTE_VALUE_MAX_LENGTH);
1308 let err = client
1309 .set_attributes([("ok".to_string(), max_value)])
1310 .await
1311 .expect_err("no server: remote call must fail after validation passes");
1312 assert!(
1313 matches!(err, Error::Remote(_)),
1314 "expected a remote error (validation accepted), got {err:?}"
1315 );
1316
1317 let max_entries: Vec<(String, String)> = (0..CLIENT_ATTRIBUTES_MAX_COUNT)
1319 .map(|i| (format!("key_{i:04}"), format!("val_{i}")))
1320 .collect();
1321 let err = client
1322 .set_attributes(max_entries)
1323 .await
1324 .expect_err("no server: remote call must fail after validation passes");
1325 assert!(
1326 matches!(err, Error::Remote(_)),
1327 "expected a remote error (validation accepted), got {err:?}"
1328 );
1329 }
1330}