Add custom Iroh relay maps
This commit is contained in:
parent
81149605bc
commit
280472cecb
10 changed files with 241 additions and 32 deletions
|
|
@ -10,3 +10,4 @@ directories.workspace = true
|
|||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml_edit.workspace = true
|
||||
url.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const DEFAULT_CONFIG_TOML: &str = r#"# geth local node config
|
||||
# Remote node-to-node communication is Iroh-only.
|
||||
#
|
||||
# relay_mode controls Iroh relay use for practical internet connectivity.
|
||||
# Values: "default", "staging", "disabled".
|
||||
# Values: "default", "staging", "disabled", "custom".
|
||||
[iroh]
|
||||
relay_mode = "default"
|
||||
# relay_map = "home"
|
||||
#
|
||||
# [iroh.relay_maps.home]
|
||||
# urls = ["https://relay.example.com"]
|
||||
"#;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -96,6 +101,18 @@ pub enum ConfigError {
|
|||
TomlParse(#[from] toml_edit::TomlError),
|
||||
#[error("invalid iroh relay_mode: {0}")]
|
||||
InvalidRelayMode(String),
|
||||
#[error("custom iroh relay_mode requires iroh.relay_map")]
|
||||
MissingRelayMapSelection,
|
||||
#[error("selected iroh relay map does not exist: {0}")]
|
||||
UnknownRelayMap(String),
|
||||
#[error("invalid iroh relay_maps table")]
|
||||
InvalidRelayMapsTable,
|
||||
#[error("invalid iroh relay map `{0}`")]
|
||||
InvalidRelayMap(String),
|
||||
#[error("iroh relay map `{0}` must include at least one URL")]
|
||||
EmptyRelayMap(String),
|
||||
#[error("invalid iroh relay URL in map `{map}`: {url}")]
|
||||
InvalidRelayUrl { map: String, url: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
|
|
@ -118,18 +135,22 @@ impl GethConfig {
|
|||
|
||||
pub fn parse(text: &str) -> Result<Self, ConfigError> {
|
||||
let document = text.parse::<toml_edit::DocumentMut>()?;
|
||||
let relay_maps = parse_relay_maps(&document)?;
|
||||
let relay_mode = match document.get("iroh").and_then(|iroh| iroh.get("relay_mode")) {
|
||||
Some(item) => {
|
||||
let value = item
|
||||
.as_str()
|
||||
.ok_or_else(|| ConfigError::InvalidRelayMode(item.to_string()))?;
|
||||
RelayMode::parse(value)?
|
||||
RelayMode::parse(value, &document, &relay_maps)?
|
||||
}
|
||||
None => RelayMode::default(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
iroh: IrohConfig { relay_mode },
|
||||
iroh: IrohConfig {
|
||||
relay_mode,
|
||||
relay_maps,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -137,36 +158,117 @@ impl GethConfig {
|
|||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct IrohConfig {
|
||||
pub relay_mode: RelayMode,
|
||||
pub relay_maps: BTreeMap<String, RelayMapConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RelayMapConfig {
|
||||
pub urls: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RelayMode {
|
||||
Disabled,
|
||||
#[default]
|
||||
Default,
|
||||
Staging,
|
||||
Custom {
|
||||
map: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl RelayMode {
|
||||
pub fn parse(value: &str) -> Result<Self, ConfigError> {
|
||||
fn parse(
|
||||
value: &str,
|
||||
document: &toml_edit::DocumentMut,
|
||||
relay_maps: &BTreeMap<String, RelayMapConfig>,
|
||||
) -> Result<Self, ConfigError> {
|
||||
match value {
|
||||
"disabled" => Ok(Self::Disabled),
|
||||
"default" => Ok(Self::Default),
|
||||
"staging" => Ok(Self::Staging),
|
||||
"custom" => {
|
||||
let map = document
|
||||
.get("iroh")
|
||||
.and_then(|iroh| iroh.get("relay_map"))
|
||||
.and_then(toml_edit::Item::as_str)
|
||||
.ok_or(ConfigError::MissingRelayMapSelection)?
|
||||
.to_owned();
|
||||
if !relay_maps.contains_key(&map) {
|
||||
return Err(ConfigError::UnknownRelayMap(map));
|
||||
}
|
||||
Ok(Self::Custom { map })
|
||||
}
|
||||
other => Err(ConfigError::InvalidRelayMode(other.to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::Disabled => "disabled",
|
||||
Self::Default => "default",
|
||||
Self::Staging => "staging",
|
||||
Self::Disabled => "disabled".to_owned(),
|
||||
Self::Default => "default".to_owned(),
|
||||
Self::Staging => "staging".to_owned(),
|
||||
Self::Custom { map } => format!("custom:{map}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_relay_maps(
|
||||
document: &toml_edit::DocumentMut,
|
||||
) -> Result<BTreeMap<String, RelayMapConfig>, ConfigError> {
|
||||
let Some(relay_maps_item) = document.get("iroh").and_then(|iroh| iroh.get("relay_maps")) else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
let relay_maps_table = relay_maps_item
|
||||
.as_table_like()
|
||||
.ok_or(ConfigError::InvalidRelayMapsTable)?;
|
||||
let mut relay_maps = BTreeMap::new();
|
||||
for (name, item) in relay_maps_table.iter() {
|
||||
let urls = parse_relay_map_urls(name, item)?;
|
||||
relay_maps.insert(name.to_owned(), RelayMapConfig { urls });
|
||||
}
|
||||
Ok(relay_maps)
|
||||
}
|
||||
|
||||
fn parse_relay_map_urls(name: &str, item: &toml_edit::Item) -> Result<Vec<String>, ConfigError> {
|
||||
let table = item
|
||||
.as_table_like()
|
||||
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||
let urls_item = table
|
||||
.get("urls")
|
||||
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||
let urls_array = urls_item
|
||||
.as_array()
|
||||
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||
let mut urls = Vec::new();
|
||||
for value in urls_array.iter() {
|
||||
let url = value
|
||||
.as_str()
|
||||
.ok_or_else(|| ConfigError::InvalidRelayMap(name.to_owned()))?;
|
||||
validate_relay_url(name, url)?;
|
||||
urls.push(url.to_owned());
|
||||
}
|
||||
if urls.is_empty() {
|
||||
return Err(ConfigError::EmptyRelayMap(name.to_owned()));
|
||||
}
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
fn validate_relay_url(map: &str, relay_url: &str) -> Result<(), ConfigError> {
|
||||
let url = url::Url::parse(relay_url).map_err(|_| ConfigError::InvalidRelayUrl {
|
||||
map: map.to_owned(),
|
||||
url: relay_url.to_owned(),
|
||||
})?;
|
||||
match url.scheme() {
|
||||
"http" | "https" => Ok(()),
|
||||
_ => Err(ConfigError::InvalidRelayUrl {
|
||||
map: map.to_owned(),
|
||||
url: relay_url.to_owned(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -183,6 +285,32 @@ mod tests {
|
|||
assert_eq!(config.iroh.relay_mode, RelayMode::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_parses_custom_relay_map() {
|
||||
let config = GethConfig::parse(
|
||||
r#"
|
||||
[iroh]
|
||||
relay_mode = "custom"
|
||||
relay_map = "home"
|
||||
|
||||
[iroh.relay_maps.home]
|
||||
urls = ["https://relay.example.com"]
|
||||
"#,
|
||||
)
|
||||
.expect("parse");
|
||||
assert_eq!(
|
||||
config.iroh.relay_mode,
|
||||
RelayMode::Custom {
|
||||
map: "home".to_owned()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
config.iroh.relay_maps["home"].urls,
|
||||
vec!["https://relay.example.com".to_owned()]
|
||||
);
|
||||
assert_eq!(config.iroh.relay_mode.label(), "custom:home");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_rejects_unknown_relay_mode() {
|
||||
let error = GethConfig::parse("[iroh]\nrelay_mode = \"magic\"\n").expect_err("error");
|
||||
|
|
@ -194,4 +322,27 @@ mod tests {
|
|||
let error = GethConfig::parse("[iroh]\nrelay_mode = true\n").expect_err("error");
|
||||
assert!(matches!(error, ConfigError::InvalidRelayMode(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_rejects_invalid_custom_relay_url() {
|
||||
let error = GethConfig::parse(
|
||||
r#"
|
||||
[iroh]
|
||||
relay_mode = "custom"
|
||||
relay_map = "home"
|
||||
|
||||
[iroh.relay_maps.home]
|
||||
urls = ["not a url"]
|
||||
"#,
|
||||
)
|
||||
.expect_err("error");
|
||||
assert!(matches!(error, ConfigError::InvalidRelayUrl { map, .. } if map == "home"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_rejects_missing_custom_relay_map() {
|
||||
let error = GethConfig::parse("[iroh]\nrelay_mode = \"custom\"\nrelay_map = \"home\"\n")
|
||||
.expect_err("error");
|
||||
assert!(matches!(error, ConfigError::UnknownRelayMap(map) if map == "home"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue