Skip to main content

iroh_services/
preset.rs

1//! An [`iroh::endpoint`] preset tailored for use with iroh-services.
2//!
3//! [`IrohServicesPreset`] starts from the n0 stock preset (production crypto
4//! provider + n0 DNS-based address lookup) and overlays the bits that
5//! iroh-services callers usually want to configure together: the relay map
6//! the endpoint should use, an optional explicit [`SecretKey`], and an
7//! optional [`ApiSecret`] that downstream code can retrieve to wire up a
8//! [`crate::Client`].
9//!
10//! # Example
11//! ```no_run
12//! use iroh::Endpoint;
13//!
14//! async fn run() -> anyhow::Result<()> {
15//!     let preset = iroh_services::preset()
16//!         .relays(["https://us-east1.project_username.iroh.link"])?
17//!         .api_secret_from_env()?
18//!         .build()?;
19//!     let endpoint = Endpoint::builder(preset).bind().await?;
20//!     Ok(())
21//! }
22//! ```
23use std::{str::FromStr, time::Duration};
24
25use anyhow::{Context, Result, anyhow};
26use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl, SecretKey, endpoint::presets::Preset};
27
28use crate::{
29    ClientBuilder,
30    api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
31    caps::{Cap, Caps, DEFAULT_CAP_EXPIRY},
32};
33
34/// An iroh endpoint preset configured for iroh-services. Build one with
35/// [`preset`] or [`IrohServicesPreset::builder`], then pass it to
36/// [`iroh::Endpoint::builder`].
37#[derive(Debug, Clone)]
38pub struct IrohServicesPreset {
39    secret_key: SecretKey,
40    relays: RelayMap,
41    // not used by the preset, only for creating a client builder
42    api_secret: ApiSecret,
43}
44
45impl IrohServicesPreset {
46    /// Start a new builder seeded with iroh-services defaults. Equivalent to
47    /// the free-standing [`preset`] function.
48    pub fn builder() -> PresetBuilder {
49        preset()
50    }
51
52    /// Returns the [`ApiSecret`] stashed on this preset, if one was set.
53    /// Useful for handing the same secret to a [`crate::Client`] without
54    /// plumbing it through twice.
55    pub fn api_secret(&self) -> &ApiSecret {
56        &self.api_secret
57    }
58
59    pub fn client_builder(&self, endpoint: &Endpoint) -> ClientBuilder {
60        ClientBuilder::new(endpoint)
61            .api_secret(self.api_secret.clone())
62            .unwrap()
63    }
64}
65
66impl Preset for IrohServicesPreset {
67    fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder {
68        // Inherit n0 defaults (crypto provider + DNS address lookup), then
69        // overlay our relay map and (optionally) an explicit secret key
70        let mut builder = iroh::endpoint::presets::N0.apply(builder);
71        builder = builder.relay_mode(RelayMode::Custom(self.relays));
72        builder = builder.secret_key(self.secret_key);
73        builder
74    }
75}
76
77/// Fluent builder for [`IrohServicesPreset`]. Construct one through
78/// [`preset`] or [`IrohServicesPreset::builder`].
79#[derive(Debug, Clone)]
80pub struct PresetBuilder {
81    cap_expiry: Duration,
82    secret_key: Option<SecretKey>,
83    relays: RelayMap,
84    api_secret: Option<ApiSecret>,
85}
86
87/// Start a new [`IrohServicesPreset`] builder seeded with iroh-services
88/// defaults: the n0 production relay map and no explicit secret key (the
89/// endpoint will generate one at bind time).
90pub fn preset() -> PresetBuilder {
91    PresetBuilder {
92        cap_expiry: DEFAULT_CAP_EXPIRY,
93        secret_key: None,
94        relays: iroh::endpoint::default_relay_mode().relay_map(),
95        api_secret: None,
96    }
97}
98
99impl PresetBuilder {
100    /// Set the endpoint's long-lived [`SecretKey`]. If left unset the
101    /// endpoint will generate a fresh random key at bind time.
102    pub fn secret_key(mut self, secret_key: SecretKey) -> Self {
103        self.secret_key = Some(secret_key);
104        self
105    }
106
107    pub fn relays<I, S>(mut self, relays: I) -> Result<Self>
108    where
109        I: IntoIterator<Item = S>,
110        S: AsRef<str>,
111    {
112        let parsed = relays
113            .into_iter()
114            .map(|s| {
115                let s = s.as_ref();
116                s.parse::<RelayUrl>()
117                    .with_context(|| format!("invalid relay url {s:?}"))
118            })
119            .collect::<anyhow::Result<Vec<_>>>()?;
120
121        self.relays = RelayMap::from_iter(parsed);
122        Ok(self)
123    }
124
125    /// Pick relays via a [`RelayMode`] (e.g. `RelayMode::Staging` or a
126    /// pre-built `RelayMode::Custom(RelayMap)`).
127    pub fn relay_mode(mut self, mode: RelayMode) -> Self {
128        self.relays = mode.relay_map();
129        self
130    }
131
132    /// Pass in a [`RelayMap`] directly, bypassing URL parsing.
133    pub fn relay_map(mut self, map: RelayMap) -> Self {
134        self.relays = map;
135        self
136    }
137
138    /// Check IROH_SERVICES_API_SECRET environment variable for a valid API secret
139    pub fn api_secret_from_env(self) -> Result<Self> {
140        let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
141        Ok(self.api_secret(ticket))
142    }
143
144    /// set client API secret from an encoded string
145    pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
146        let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
147        Ok(self.api_secret(key))
148    }
149
150    /// Stash an [`ApiSecret`] on the preset so callers can retrieve it later
151    /// via [`IrohServicesPreset::api_secret`] when constructing a client.
152    pub fn api_secret(mut self, api_secret: ApiSecret) -> Self {
153        self.api_secret = Some(api_secret);
154        self
155    }
156
157    /// Finalize the configuration into an [`IrohServicesPreset`].
158    pub fn build(self) -> Result<IrohServicesPreset> {
159        let secret_key = self.secret_key.unwrap_or_else(SecretKey::generate);
160
161        let Some(api_secret) = self.api_secret else {
162            return Err(anyhow!(
163                "api secret is required to use iroh_services relay preset"
164            ));
165        };
166
167        // build our token to set on relays
168        let rcan = crate::caps::create_api_token_from_secret_key(
169            api_secret.secret.clone(),
170            secret_key.public(),
171            self.cap_expiry,
172            Caps::new([Cap::Relay(crate::caps::RelayCap::Use)]),
173        )?;
174
175        let mut token = data_encoding::BASE32_NOPAD.encode(&rcan.encode());
176        token.make_ascii_lowercase();
177
178        let relays = self.relays.with_auth_token(token);
179
180        Ok(IrohServicesPreset {
181            secret_key,
182            relays,
183            api_secret,
184        })
185    }
186}