use std::{str::FromStr, time::Duration};
use anyhow::{Context, Result, anyhow};
use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl, SecretKey, endpoint::presets::Preset};
use crate::{
ClientBuilder,
api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret},
caps::{Cap, Caps, DEFAULT_CAP_EXPIRY},
};
#[derive(Debug, Clone)]
pub struct IrohServicesPreset {
secret_key: SecretKey,
relays: RelayMap,
api_secret: ApiSecret,
}
impl IrohServicesPreset {
pub fn builder() -> PresetBuilder {
preset()
}
pub fn api_secret(&self) -> &ApiSecret {
&self.api_secret
}
pub fn client_builder(&self, endpoint: &Endpoint) -> ClientBuilder {
ClientBuilder::new(endpoint)
.api_secret(self.api_secret.clone())
.unwrap()
}
}
impl Preset for IrohServicesPreset {
fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder {
let mut builder = iroh::endpoint::presets::N0.apply(builder);
builder = builder.relay_mode(RelayMode::Custom(self.relays));
builder = builder.secret_key(self.secret_key);
builder
}
}
#[derive(Debug, Clone)]
pub struct PresetBuilder {
cap_expiry: Duration,
secret_key: Option<SecretKey>,
relays: RelayMap,
api_secret: Option<ApiSecret>,
}
pub fn preset() -> PresetBuilder {
PresetBuilder {
cap_expiry: DEFAULT_CAP_EXPIRY,
secret_key: None,
relays: iroh::endpoint::default_relay_mode().relay_map(),
api_secret: None,
}
}
impl PresetBuilder {
pub fn secret_key(mut self, secret_key: SecretKey) -> Self {
self.secret_key = Some(secret_key);
self
}
pub fn relays<I, S>(mut self, relays: I) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let parsed = relays
.into_iter()
.map(|s| {
let s = s.as_ref();
s.parse::<RelayUrl>()
.with_context(|| format!("invalid relay url {s:?}"))
})
.collect::<anyhow::Result<Vec<_>>>()?;
self.relays = RelayMap::from_iter(parsed);
Ok(self)
}
pub fn relay_mode(mut self, mode: RelayMode) -> Self {
self.relays = mode.relay_map();
self
}
pub fn relay_map(mut self, map: RelayMap) -> Self {
self.relays = map;
self
}
pub fn api_secret_from_env(self) -> Result<Self> {
let ticket = ApiSecret::from_env_var(API_SECRET_ENV_VAR_NAME)?;
Ok(self.api_secret(ticket))
}
pub fn api_secret_from_str(self, secret_key: &str) -> Result<Self> {
let key = ApiSecret::from_str(secret_key).context("invalid iroh services api secret")?;
Ok(self.api_secret(key))
}
pub fn api_secret(mut self, api_secret: ApiSecret) -> Self {
self.api_secret = Some(api_secret);
self
}
pub fn build(self) -> Result<IrohServicesPreset> {
let secret_key = self.secret_key.unwrap_or_else(SecretKey::generate);
let Some(api_secret) = self.api_secret else {
return Err(anyhow!(
"api secret is required to use iroh_services relay preset"
));
};
let rcan = crate::caps::create_api_token_from_secret_key(
api_secret.secret.clone(),
secret_key.public(),
self.cap_expiry,
Caps::new([Cap::Relay(crate::caps::RelayCap::Use)]),
)?;
let mut token = data_encoding::BASE32_NOPAD.encode(&rcan.encode());
token.make_ascii_lowercase();
let relays = self.relays.with_auth_token(token);
Ok(IrohServicesPreset {
secret_key,
relays,
api_secret,
})
}
}