-
Notifications
You must be signed in to change notification settings - Fork 1
feat: iroh-services relay preset_builder #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| //! This example shows common methods for configuring custom relays provided by iroh services. | ||
| //! All of these leverage the [`iroh_services::preset`] builder to configure the endpoint, | ||
| //! which itself builds an [`iroh::presets::Preset`] to pass to an [`iroh::Endpoint`] on | ||
| //! construction. | ||
| //! | ||
| //! To run these, set IROH_SERVICES_API_SECRET in your environment, using an API secret | ||
| //! from your iroh services project, and then run with `cargo run --example relays`. | ||
| //! | ||
| //! Your app will use only one of these methods, depending on your use case. To test this | ||
| //! example with custom relay URLS, you will need to comment out the secret key preset | ||
| //! example and use the `relays` method instead, pasting in your own relay URLs. | ||
| use iroh::{Endpoint, SecretKey}; | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> anyhow::Result<()> { | ||
| // minimal preset that works with a free iroh services project. This will | ||
| // give a bandwidth bump when using the public relays, and surface your | ||
| // relay traffic on your project: | ||
| let _preset = iroh_services::preset().api_secret_from_env()?.build()?; | ||
|
|
||
| // pro & enterprise projects have access to custom relays, which are set | ||
| // with the `relays` method on the builder. | ||
| // You'll need to replace these strings with the relay urls for your project. | ||
| let _preset = iroh_services::preset() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this example actually runnable? It seems like all of the other examples in this repository can be run by setting the API SECRET in env without modifying the code. I'd prefer to keep it that way, and instead put this information in the documentation (https://docs.iroh.computer) and link to it from the README of this repository as well as the relays page on iroh services.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep it's runnable! Or at least, it is on my dev branch & should be generally. I added a note about setting the IROH_SERVICES_API_SECRET in the example description in c124d31 |
||
| .relays([ | ||
| "https://use1-1.relay.username.project.iroh.link", | ||
| "https://usw1-1.relay.username.project.iroh.link", | ||
| "https://euc1-1.relay.username.project.iroh.link", | ||
| ])? | ||
| .api_secret_from_env()? | ||
| .build()?; | ||
|
|
||
| // if you are using a secret key from disk, or generally want control over | ||
| // the ID your endpoint uses, the secret key must be given to the preset | ||
| // before passing it to the endpoint. The access token that the preset creates | ||
| // to access relays is scoped only to the given key. | ||
| // | ||
| // If no key is provided, a random one is generated and passed to the | ||
| // endpoint. | ||
| let secret_key = SecretKey::generate(); | ||
| let preset = iroh_services::preset() | ||
| .secret_key(secret_key) | ||
| .api_secret_from_env()? | ||
| .build()?; | ||
|
|
||
| // once a preset is built, we'll pass it to the endpoint for binding. | ||
| // we clone the preset so we can reuse to get a client builder below | ||
| let endpoint = Endpoint::bind(preset.clone()).await?; | ||
|
|
||
| // wait for the endpoint to be online, to prove we have an authorized | ||
| // connection to a relay | ||
| endpoint.online().await; | ||
|
|
||
| // a client is not required to use, but the preset has a convenience method | ||
| // for creating a client builder that uses the same access token as the | ||
| // endpoint, so you don't need to pass the secret key separately: | ||
| let client = preset.client_builder(&endpoint).build().await?; | ||
|
|
||
| // we can also ping the service just to confirm everything is working | ||
| client.ping().await?; | ||
|
|
||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,9 @@ use n0_future::time::Duration; | |
| use rcan::{Capability, Expires, Rcan}; | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| /// How long tokens are valid for by default | ||
| pub(crate) const DEFAULT_CAP_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 1 month | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe use
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no such method I'm afraid, but I can bump up to |
||
|
|
||
| macro_rules! cap_enum( | ||
| ($enum:item) => { | ||
| #[derive( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ mod openssh; | |
| pub mod api_secret; | ||
| pub mod caps; | ||
| pub mod net_diagnostics; | ||
| pub mod preset; | ||
| pub mod protocol; | ||
|
|
||
| mod built_info { | ||
|
|
@@ -59,8 +60,9 @@ pub use client_host::{CLIENT_HOST_ALPN, ClientHost, ClientHostClient}; | |
| pub use iroh_metrics::Registry; | ||
|
|
||
| pub use self::{ | ||
| api_secret::ApiSecret, | ||
| client::{API_SECRET_ENV_VAR_NAME, Client, ClientBuilder}, | ||
| api_secret::{API_SECRET_ENV_VAR_NAME, ApiSecret}, | ||
| client::{Client, ClientBuilder}, | ||
| net_diagnostics::{DiagnosticsReport, checks::run_diagnostics}, | ||
| preset::{IrohServicesPreset, PresetBuilder, preset}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know we have the bad precedent, but having a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if this fully addresses it, but I've made |
||
| protocol::ALPN, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| //! An [`iroh::endpoint`] preset tailored for use with iroh-services. | ||
| //! | ||
| //! [`IrohServicesPreset`] starts from the n0 stock preset (production crypto | ||
| //! provider + n0 DNS-based address lookup) and overlays the bits that | ||
| //! iroh-services callers usually want to configure together: the relay map | ||
| //! the endpoint should use, an optional explicit [`SecretKey`], and an | ||
| //! optional [`ApiSecret`] that downstream code can retrieve to wire up a | ||
| //! [`crate::Client`]. | ||
| //! | ||
| //! # Example | ||
| //! ```no_run | ||
| //! use iroh::Endpoint; | ||
| //! | ||
| //! async fn run() -> anyhow::Result<()> { | ||
| //! let preset = iroh_services::preset() | ||
| //! .relays(["https://us-east1.project_username.iroh.link"])? | ||
| //! .api_secret_from_env()? | ||
| //! .build()?; | ||
| //! let endpoint = Endpoint::builder(preset).bind().await?; | ||
| //! Ok(()) | ||
| //! } | ||
| //! ``` | ||
| 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}, | ||
| }; | ||
|
|
||
| /// An iroh endpoint preset configured for iroh-services. Build one with | ||
| /// [`preset`] or [`IrohServicesPreset::builder`], then pass it to | ||
| /// [`iroh::Endpoint::builder`]. | ||
| #[derive(Debug, Clone)] | ||
| pub struct IrohServicesPreset { | ||
| secret_key: SecretKey, | ||
| relays: RelayMap, | ||
| // not used by the preset, only for creating a client builder | ||
| api_secret: ApiSecret, | ||
| } | ||
|
|
||
| impl IrohServicesPreset { | ||
| /// Start a new builder seeded with iroh-services defaults. Equivalent to | ||
| /// the free-standing [`preset`] function. | ||
| pub fn builder() -> PresetBuilder { | ||
| preset() | ||
| } | ||
|
|
||
| /// Returns the [`ApiSecret`] stashed on this preset, if one was set. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| /// Useful for handing the same secret to a [`crate::Client`] without | ||
| /// plumbing it through twice. | ||
| pub fn api_secret(&self) -> &ApiSecret { | ||
| &self.api_secret | ||
| } | ||
|
|
||
| /// Returns a [`ClientBuilder`] pre-configured with this preset's API secret. | ||
| pub fn client_builder(&self, endpoint: &Endpoint) -> ClientBuilder { | ||
| ClientBuilder::new(endpoint) | ||
| .api_secret(self.api_secret.clone()) | ||
| .unwrap() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you dig into the builder, the |
||
| } | ||
| } | ||
|
|
||
| impl Preset for IrohServicesPreset { | ||
| fn apply(self, builder: iroh::endpoint::Builder) -> iroh::endpoint::Builder { | ||
| // Inherit n0 defaults (crypto provider + DNS address lookup), then | ||
| // overlay our relay map and (optionally) an explicit secret key | ||
| 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 | ||
| } | ||
| } | ||
|
|
||
| /// Fluent builder for [`IrohServicesPreset`]. Construct one through | ||
| /// [`preset`] or [`IrohServicesPreset::builder`]. | ||
| #[derive(Debug, Clone)] | ||
| pub struct PresetBuilder { | ||
| cap_expiry: Duration, | ||
| secret_key: Option<SecretKey>, | ||
| relays: RelayMap, | ||
| api_secret: Option<ApiSecret>, | ||
| } | ||
|
|
||
| /// Start a new [`IrohServicesPreset`] builder seeded with iroh-services | ||
| /// defaults: the n0 production relay map and no explicit secret key (the | ||
| /// endpoint will generate one at bind time). | ||
| 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 { | ||
| /// Set the endpoint's long-lived [`SecretKey`]. If left unset the | ||
| /// endpoint will generate a fresh random key at bind time. | ||
| 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> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing docs
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added an example to help make the generic argument more understandable |
||
| 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) | ||
| } | ||
|
|
||
| /// Pick relays via a [`RelayMode`] (e.g. `RelayMode::Staging` or a | ||
| /// pre-built `RelayMode::Custom(RelayMap)`). | ||
| pub fn relay_mode(mut self, mode: RelayMode) -> Self { | ||
| self.relays = mode.relay_map(); | ||
| self | ||
| } | ||
|
|
||
| /// Pass in a [`RelayMap`] directly, bypassing URL parsing. | ||
| pub fn relay_map(mut self, map: RelayMap) -> Self { | ||
| self.relays = map; | ||
| self | ||
| } | ||
|
|
||
| /// Check IROH_SERVICES_API_SECRET environment variable for a valid API secret | ||
| 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)) | ||
| } | ||
|
|
||
| /// set client API secret from an encoded string | ||
| 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)) | ||
| } | ||
|
|
||
| /// Stash an [`ApiSecret`] on the preset so callers can retrieve it later | ||
| /// via [`IrohServicesPreset::api_secret`] when constructing a client. | ||
| pub fn api_secret(mut self, api_secret: ApiSecret) -> Self { | ||
| self.api_secret = Some(api_secret); | ||
| self | ||
| } | ||
|
|
||
| /// Finalize the configuration into an [`IrohServicesPreset`]. | ||
| 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" | ||
| )); | ||
| }; | ||
|
|
||
| // build our token to interact with relays. This is only scoped to relay use. | ||
| 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, | ||
| }) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.