node: Make node aliases required

This is a fairly substantial change, which adds a new configuration file
to the user's `$RAD_HOME` that includes node configuration, including
the alias, and also makes node aliases required.

When running `rad auth`, the user is now prompted for an alias, which
defaults to `$USER`.

When running `rad self`, the alias is now shown.

If the user runs radicle without a config file, the defaults are loaded,
and `$USER` is used as the alias.
This commit is contained in:
Alexis Sellier 2023-07-04 00:26:10 +02:00
parent 8ed55a1925
commit eb701be033
No known key found for this signature in database
53 changed files with 898 additions and 614 deletions

View File

@ -1,30 +1,30 @@
```
$ cd heartwood
$ git branch -r
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
rad/master
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
```
$ git ls-remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
$ git ls-remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi 'refs/heads/*'
145e1e69bef3ad93d14946ea212249c2fa9b9828 refs/heads/alice/1
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 refs/heads/master
```
``` (stderr)
$ git fetch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
$ git fetch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
* [new branch] alice/1 -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
* [new branch] alice/1 -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
```
```
$ git branch -r
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
rad/master
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
```
$ git rev-parse z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
$ git rev-parse alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/alice/1
145e1e69bef3ad93d14946ea212249c2fa9b9828
```

View File

@ -2,7 +2,7 @@ Initializing a new identity with `rad-auth`.
The example below is run with `RAD_PASSPHRASE` set.
```
$ rad auth
$ rad auth --alias "alice"
Initializing your radicle 👾 identity
@ -18,3 +18,9 @@ You can get the above information at all times using the `self` command:
$ rad self --did
did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
```
You can also show your alias:
```
$ rad self --alias
alice
```

View File

@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope all
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkux1…nVhib7Z..
✓ Creating checkout in ./heartwood..
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [..]/heartwood/
```
@ -68,10 +68,10 @@ And fetch his refs:
```
$ git fetch --all
Fetching rad
Fetching z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
Fetching alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
Fetching bob
$ git branch --remotes
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
bob/master
rad/master
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

View File

@ -7,8 +7,8 @@ $ rad clone rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --scope trusted
✓ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from z6MknSL…StBU8Vi..
✓ Forking under z6Mkt67…v4N1tRk..
✓ Creating checkout in ./heartwood..
✓ Remote z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Remote alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi added
✓ Remote-tracking branch alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master created for z6MknSL…StBU8Vi
✓ Repository successfully cloned under [..]/heartwood/
```
@ -27,8 +27,8 @@ Let's check that the remote tracking branch was setup correctly:
```
$ git branch --remotes
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
rad/master
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
The first branch is ours, and the second points to the repository delegate.
@ -36,10 +36,10 @@ We can also take a look at the remotes:
```
$ git remote -v
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
rad rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji (fetch)
rad rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (push)
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (fetch)
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (push)
```
Let's check the last commit!

View File

@ -2,7 +2,7 @@ The `rad inspect` command can be run without being authenticated with radicle:
``` (fail)
$ rad self
✗ Self failed: Could not load radicle profile
✗ Self failed: Could not load radicle profile: no profile found at path [..]
✗ Hint: To setup your radicle profile, run `rad auth`.
```

View File

@ -67,12 +67,12 @@ To rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkE
```
```
$ rad patch --merged
╭──────────────────────────────────────────────────────────────────────────────────╮
│ ● ID Title Author Head + - Updated │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ✔ 0ec956c First change z6MknSL…StBU8Vi (you) 20aa5dd +0 -0 [ ... ] │
│ ✔ 928d76e Second change z6MknSL…StBU8Vi (you) daf349f +0 -0 [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────
│ ● ID Title Author Head + - Updated │
├────────────────────────────────────────────────────────────────────────────────────────
│ ✔ 0ec956c First change z6MknSL…StBU8Vi alice (you) 20aa5dd +0 -0 [ ... ] │
│ ✔ 928d76e Second change z6MknSL…StBU8Vi alice (you) daf349f +0 -0 [ ... ] │
╰────────────────────────────────────────────────────────────────────────────────────────
```
We can verify that the remote tracking branches were also deleted:

View File

@ -48,6 +48,6 @@ able to fetch the node alias from our db!
```
$ rad remote add did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
✓ Remote bob added
✓ Remote-tracking branch bob/master created for z6Mkt67…v4N1tRk
✓ Remote bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk added
✓ Remote-tracking branch bob@z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk/master created for z6Mkt67…v4N1tRk
```

View File

@ -3,12 +3,14 @@ device and node.
```
$ rad self
Alias alice
DID did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└╴Node ID (NID) z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
SSH not running
├╴Key (hash) SHA256:UIedaL6Cxm6OUErh9GQUzzglSk7VpQlVTI1TAFB/HWA
└╴Key (full) ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHahWSBEpuT1ESZbynOmBNkLBSnR32Ar4woZqSV2YNH1
Home [..]/home/alice/.radicle
├╴Config [..]/home/alice/.radicle/config.json
├╴Storage [..]/home/alice/.radicle/storage
├╴Keys [..]/home/alice/.radicle/keys
└╴Node [..]/home/alice/.radicle/node

View File

@ -112,7 +112,7 @@ $ rad patch show 50e29a1
│ ● opened by bob (z6Mkt67…v4N1tRk) [ ... ] │
│ ↑ updated to 3530243d46a2e7a8e4eac7afcbb17cc7c56b3d29 (27857ec) [ ... ] │
│ ↑ updated to 744c1f0a75b1c42833c9aa32f79cd40443925d66 (f567f69) [ ... ] │
│ ✓ merged by (you) [ ... ]
│ ✓ merged by alice (you) [ ... ] │
╰──────────────────────────────────────────────────────────────────────────────╯
```

View File

@ -24,7 +24,7 @@ $ git pull --all --ff
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji
f2de534..f567f69 master -> rad/master
From rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
f2de534..f567f69 master -> z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
f2de534..f567f69 master -> alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```
Now our master branch is up to date with the maintainer's master:
@ -32,5 +32,5 @@ Now our master branch is up to date with the maintainer's master:
```
$ git rev-parse master
f567f695d25b4e8fb63b5f5ad2a584529826e908
$ git diff master..z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
$ git diff master..alice@z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/master
```

View File

@ -1,10 +1,13 @@
#![allow(clippy::or_fun_call)]
use std::env;
use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow;
use radicle::crypto::ssh;
use radicle::crypto::ssh::Passphrase;
use radicle::node::Alias;
use radicle::profile::env::RAD_PASSPHRASE;
use radicle::{profile, Profile};
@ -26,6 +29,7 @@ Usage
Options
--alias When initializing an identity, sets the node alias
--stdin Read passphrase from stdin (default: false)
--help Print help
"#,
@ -34,6 +38,7 @@ Options
#[derive(Debug)]
pub struct Options {
pub stdin: bool,
pub alias: Option<Alias>,
}
impl Args for Options {
@ -41,10 +46,17 @@ impl Args for Options {
use lexopt::prelude::*;
let mut stdin = false;
let mut alias = None;
let mut parser = lexopt::Parser::from_args(args);
while let Some(arg) = parser.next()? {
match arg {
Long("alias") => {
let val = parser.value()?;
let val = term::args::alias(&val)?;
alias = Some(val);
}
Long("stdin") => {
stdin = true;
}
@ -55,7 +67,7 @@ impl Args for Options {
}
}
Ok((Options { stdin }, vec![]))
Ok((Options { alias, stdin }, vec![]))
}
}
@ -81,6 +93,16 @@ pub fn init(options: Options) -> anyhow::Result<()> {
anyhow::bail!("Error retrieving git version; please check your installation");
}
let alias: Alias = if let Some(alias) = options.alias {
alias
} else {
let user = env::var("USER").ok().and_then(|u| Alias::from_str(&u).ok());
term::input(
"Enter your alias:",
user,
Some("This is your node alias. You can always change it later"),
)?
};
let home = profile::home()?;
let passphrase = if options.stdin {
term::passphrase_stdin()
@ -88,7 +110,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE)
}?;
let spinner = term::spinner("Creating your Ed25519 keypair...");
let profile = Profile::init(home, passphrase.clone())?;
let profile = Profile::init(home, alias, passphrase.clone())?;
spinner.finish();
match ssh::agent::Agent::connect() {

View File

@ -160,14 +160,16 @@ pub fn setup_remote(
remote_name: Option<git::RefString>,
aliases: &impl AliasStore,
) -> anyhow::Result<()> {
let remote_name = if let Some(alias) = remote_name {
alias
let remote_name = if let Some(name) = remote_name {
name
} else {
let alias = aliases
.alias(remote_id)
.unwrap_or_else(|| remote_id.to_string());
git::RefString::try_from(alias.clone())
.map_err(|_| anyhow!("invalid remote name: '{alias}'"))?
let name = if let Some(alias) = aliases.alias(remote_id) {
format!("{alias}@{remote_id}")
} else {
remote_id.to_human()
};
git::RefString::try_from(name.as_str())
.map_err(|_| anyhow!("invalid remote name: '{name}'"))?
};
let (remote, branch) = setup.run(remote_name, *remote_id)?;

View File

@ -198,14 +198,14 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
let name = options.name.unwrap_or_else(|| {
let default = path.file_name().map(|f| f.to_string_lossy().to_string());
term::input("Name", default).unwrap()
term::input("Name", default, None).unwrap()
});
let description = options
.description
.unwrap_or_else(|| term::input("Description", None).unwrap());
.unwrap_or_else(|| term::input("Description", None, None).unwrap());
let branch = options.branch.unwrap_or_else(|| {
if interactive.yes() {
term::input("Default branch", Some(head)).unwrap()
term::input("Default branch", Some(head), None).unwrap()
} else {
head
}

View File

@ -462,6 +462,7 @@ fn list(
]);
}
table.print();
Ok(())
}

View File

@ -53,7 +53,7 @@ fn print_nodes(store: &tracking::store::Config) -> anyhow::Result<()> {
term::format::highlight(Did::from(id).to_string()),
match alias {
None => term::format::secondary(String::from("n/a")),
Some(alias) => term::format::secondary(alias),
Some(alias) => term::format::secondary(alias.to_string()),
},
term::format::secondary(policy.to_string()),
]);

View File

@ -93,7 +93,7 @@ pub fn run(
/// Patch row.
pub fn row(
profile: &Profile,
alias: Option<String>,
alias: Option<Alias>,
id: &PatchId,
patch: &Patch,
repository: &Repository,

View File

@ -18,6 +18,7 @@ Usage
Options
--alias Show your Node alias
--nid Show your Node ID (NID)
--did Show your DID
--home Show your Radicle home
@ -29,6 +30,7 @@ Options
#[derive(Debug)]
enum Show {
Alias,
NodeId,
Did,
Home,
@ -51,6 +53,9 @@ impl Args for Options {
while let Some(arg) = parser.next()? {
match arg {
Long("alias") if show.is_none() => {
show = Some(Show::Alias);
}
Long("nid") if show.is_none() => {
show = Some(Show::NodeId);
}
@ -86,6 +91,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
match options.show {
Show::Alias => {
term::print(profile.config.alias());
}
Show::NodeId => {
term::print(profile.id());
}
@ -110,6 +118,11 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
fn all(profile: &Profile) -> anyhow::Result<()> {
let mut table = term::Table::default();
table.push([
term::format::style("Alias").to_string(),
term::format::primary(profile.config.alias()).to_string(),
]);
let did = profile.did();
table.push([
term::format::style("DID").to_string(),
@ -153,6 +166,12 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
term::format::tertiary(home.display()).to_string(),
]);
let config_path = profile.home.config();
table.push([
term::format::style("├╴Config").to_string(),
term::format::tertiary(config_path.display()).to_string(),
]);
let storage_path = profile.home.storage();
table.push([
term::format::style("├╴Storage").to_string(),

View File

@ -79,10 +79,7 @@ impl Args for Options {
}
(Long("alias"), Some(Operation::TrackNode { alias, .. })) => {
let name = parser.value()?;
let name = name
.to_str()
.to_owned()
.ok_or_else(|| anyhow!("alias specified is not UTF-8"))?;
let name = term::args::alias(&name)?;
*alias = Some(name.to_owned());
}

View File

@ -113,14 +113,13 @@ where
/// Get the default profile. Fails if there is no profile.
pub fn profile() -> Result<Profile, anyhow::Error> {
let error = args::Error::WithHint {
err: anyhow::anyhow!("Could not load radicle profile"),
hint: "To setup your radicle profile, run `rad auth`.",
};
match Profile::load() {
Ok(profile) => Ok(profile),
Err(_) => Err(error.into()),
Err(e) => Err(args::Error::WithHint {
err: anyhow::anyhow!("Could not load radicle profile: {e}"),
hint: "To setup your radicle profile, run `rad auth`.",
}
.into()),
}
}

View File

@ -6,7 +6,7 @@ use anyhow::anyhow;
use radicle::cob::{self, issue, patch};
use radicle::crypto;
use radicle::git::RefString;
use radicle::node::Address;
use radicle::node::{Address, Alias};
use radicle::prelude::{Did, Id, NodeId};
#[derive(thiserror::Error, Debug)]
@ -136,6 +136,15 @@ pub fn string(val: &OsString) -> String {
val.to_string_lossy().to_string()
}
pub fn alias(val: &OsString) -> anyhow::Result<Alias> {
let val = val.as_os_str();
let val = val
.to_str()
.ok_or_else(|| anyhow!("alias must be valid UTF-8"))?;
Alias::from_str(val).map_err(|e| e.into())
}
pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
let val = val.to_string_lossy();
issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))

View File

@ -4,7 +4,7 @@ pub use radicle_term::format::*;
pub use radicle_term::{style, Paint};
use radicle::cob::{ObjectId, Timestamp};
use radicle::node::{AliasStore, NodeId};
use radicle::node::{Alias, AliasStore, NodeId};
use radicle::prelude::Did;
use radicle::profile::Profile;
use radicle_term::element::Line;
@ -143,15 +143,15 @@ impl<'a> fmt::Display for Identity<'a> {
pub enum Author<'a> {
Author {
nid: &'a NodeId,
alias: Option<String>,
alias: Option<Alias>,
},
Me {
alias: Option<String>,
alias: Option<Alias>,
},
}
impl<'a> Author<'a> {
pub fn new(nid: &'a NodeId, alias: Option<String>, me: &Profile) -> Author<'a> {
pub fn new(nid: &'a NodeId, alias: Option<Alias>, me: &Profile) -> Author<'a> {
if nid == me.id() {
Self::Me { alias }
} else {

View File

@ -3,6 +3,7 @@ use std::str::FromStr;
use std::{env, fs, thread, time};
use radicle::git;
use radicle::node::Alias;
use radicle::node::Handle as _;
use radicle::prelude::Id;
use radicle::profile::Home;
@ -48,6 +49,7 @@ fn test<'a>(
.env("EDITOR", "true")
.env("TZ", "UTC")
.env("LANG", "C")
.env("USER", "alice")
.env(radicle_cob::git::RAD_COMMIT_TIME, "1671125284")
.envs(git::env::GIT_DEFAULT_CONFIG)
.envs(envs)
@ -234,12 +236,12 @@ fn rad_node() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = tempfile::tempdir().unwrap();
let alice = alice.spawn(Config::default());
let _bob = bob.spawn(Config::default());
let alice = alice.spawn();
let _bob = bob.spawn();
fixtures::repository(working.path().join("alice"));
@ -398,9 +400,9 @@ fn rad_rm() {
#[test]
fn rad_track() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let alice = environment.node(Config::new(Alias::new("alice")));
let working = tempfile::tempdir().unwrap();
let alice = alice.spawn(Config::default());
let alice = alice.spawn();
test(
"examples/rad-track.md",
@ -416,15 +418,15 @@ fn rad_clone() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let mut alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = environment.tmp().join("working");
// Setup a test project.
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
// Prevent Alice from fetching Bob's fork, as we're not testing that and it may cause errors.
alice.handle.track_repo(acme, Scope::Trusted).unwrap();
@ -438,17 +440,17 @@ fn rad_clone_all() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let eve = environment.node("eve");
let mut alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let eve = environment.node(Config::new(Alias::new("eve")));
let working = environment.tmp().join("working");
// Setup a test project.
let acme = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut eve = eve.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
alice.handle.track_repo(acme, Scope::All).unwrap();
bob.connect(&alice).converge([&alice]);
@ -477,7 +479,7 @@ fn rad_clone_all() {
#[test]
fn rad_self() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let alice = environment.node(Config::new(Alias::new("alice")));
let working = environment.tmp().join("working");
test("examples/rad-self.md", working, Some(&alice.home), []).unwrap();
@ -488,10 +490,10 @@ fn rad_clone_unknown() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let alice = environment.node("alice");
let alice = environment.node(Config::new(Alias::new("alice")));
let working = environment.tmp().join("working");
let alice = alice.spawn(Config::default());
let alice = alice.spawn();
test(
"examples/rad-clone-unknown.md",
@ -507,12 +509,12 @@ fn rad_init_sync_and_clone() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = environment.tmp().join("working");
let alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let alice = alice.spawn();
let mut bob = bob.spawn();
bob.connect(&alice);
@ -548,11 +550,11 @@ fn rad_init_sync_and_clone() {
fn rad_fetch() {
let mut environment = Environment::new();
let working = environment.tmp().join("working");
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let mut alice = alice.spawn(Config::default());
let bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
fixtures::repository(working.join("alice"));
@ -582,11 +584,11 @@ fn rad_fetch() {
fn rad_fork() {
let mut environment = Environment::new();
let working = environment.tmp().join("working");
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let mut alice = alice.spawn(Config::default());
let bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
fixtures::repository(working.join("alice"));
@ -625,10 +627,10 @@ fn test_clone_without_seeds() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let mut alice = environment.node("alice");
let mut alice = environment.node(Config::new(Alias::new("alice")));
let working = environment.tmp().join("working");
let rid = alice.project("heartwood", "Radicle Heartwood Protocol & Stack");
let mut alice = alice.spawn(Config::default());
let mut alice = alice.spawn();
let seeds = alice.handle.seeds(rid).unwrap();
assert!(!seeds.has_connections());
@ -648,13 +650,13 @@ fn test_cob_replication() {
let mut environment = Environment::new();
let working = tempfile::tempdir().unwrap();
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let mut alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let rid = alice.project("heartwood", "");
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let events = alice.handle.events();
alice.handle.track_node(bob.id, None).unwrap();
@ -708,13 +710,13 @@ fn test_cob_replication() {
fn test_cob_deletion() {
let mut environment = Environment::new();
let working = tempfile::tempdir().unwrap();
let mut alice = environment.node("alice");
let bob = environment.node("bob");
let mut alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let rid = alice.project("heartwood", "");
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.handle.track_repo(rid, Scope::All).unwrap();
bob.handle.track_repo(rid, Scope::All).unwrap();
@ -762,9 +764,9 @@ fn rad_sync() {
let mut environment = Environment::new();
let working = environment.tmp().join("working");
let alice = environment.node("alice");
let bob = environment.node("bob");
let eve = environment.node("eve");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let eve = environment.node(Config::new(Alias::new("eve")));
let acme = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
fixtures::repository(working.join("acme"));
@ -777,9 +779,9 @@ fn rad_sync() {
)
.unwrap();
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut eve = eve.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
bob.handle.track_repo(acme, Scope::All).unwrap();
eve.handle.track_repo(acme, Scope::All).unwrap();
@ -806,19 +808,19 @@ fn rad_sync() {
//
fn test_replication_via_seed() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let seed = environment.node("seed");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let seed = environment.node(Config {
policy: Policy::Track,
scope: Scope::All,
..Config::new(Alias::new("seed"))
});
let working = environment.tmp().join("working");
let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let seed = seed.spawn(Config {
policy: Policy::Track,
scope: Scope::All,
..Config::default()
});
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let seed = seed.spawn();
alice.connect(&seed);
bob.connect(&seed);
@ -891,8 +893,8 @@ fn test_replication_via_seed() {
#[test]
fn rad_remote() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = environment.tmp().join("working");
let home = alice.home.clone();
let rid = Id::from_str("z42hL2jL4XNk6K8oHQaSWfMgCL7ji").unwrap();
@ -907,11 +909,11 @@ fn rad_remote() {
)
.unwrap();
let mut alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice
.handle
.track_node(bob.id, Some("bob".to_owned()))
.track_node(bob.id, Some(Alias::new("bob")))
.unwrap();
bob.connect(&alice);
@ -935,7 +937,7 @@ fn rad_merge_via_push() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let alice = environment.node("alice");
let alice = environment.node(Config::new(Alias::new("alice")));
let working = environment.tmp().join("working");
fixtures::repository(working.join("alice"));
@ -948,7 +950,7 @@ fn rad_merge_via_push() {
)
.unwrap();
let alice = alice.spawn(Config::default());
let alice = alice.spawn();
test(
"examples/rad-merge-via-push.md",
@ -964,8 +966,8 @@ fn git_push_and_pull() {
logger::init(log::Level::Debug);
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = environment.tmp().join("working");
fixtures::repository(working.join("alice"));
@ -978,8 +980,8 @@ fn git_push_and_pull() {
)
.unwrap();
let alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config::default());
let alice = alice.spawn();
let mut bob = bob.spawn();
bob.connect(&alice).converge([&alice]);
@ -1016,8 +1018,8 @@ fn git_push_and_pull() {
#[test]
fn rad_workflow() {
let mut environment = Environment::new();
let alice = environment.node("alice");
let bob = environment.node("bob");
let alice = environment.node(Config::new(Alias::new("alice")));
let bob = environment.node(Config::new(Alias::new("bob")));
let working = environment.tmp().join("working");
fixtures::repository(working.join("alice"));
@ -1030,11 +1032,8 @@ fn rad_workflow() {
)
.unwrap();
let alice = alice.spawn(Config::default());
let mut bob = bob.spawn(Config {
alias: Some("bob".to_string()),
..Default::default()
});
let alice = alice.spawn();
let mut bob = bob.spawn();
bob.connect(&alice).converge([&alice]);

View File

@ -14,7 +14,7 @@ use radicle::cob::thread;
use radicle::cob::thread::CommentId;
use radicle::cob::{ActorId, Author, Reaction, Timestamp};
use radicle::git::RefString;
use radicle::node::AliasStore;
use radicle::node::{Alias, AliasStore};
use radicle::prelude::NodeId;
use radicle::storage::{git, refs, ReadRepository};
use radicle_surf::blob::Blob;
@ -146,7 +146,7 @@ pub(crate) fn patch(
}
/// Returns JSON for an `author` and fills in `alias` when present.
fn author(author: &Author, alias: Option<String>) -> Value {
fn author(author: &Author, alias: Option<Alias>) -> Value {
match alias {
Some(alias) => json!({
"id": author.id,
@ -157,7 +157,7 @@ fn author(author: &Author, alias: Option<String>) -> Value {
}
/// Returns JSON for a patch `Merge` and fills in `alias` when present.
fn merge(merge: &Merge, nid: &NodeId, alias: Option<String>) -> Value {
fn merge(merge: &Merge, nid: &NodeId, alias: Option<Alias>) -> Value {
match alias {
Some(alias) => json!({
"author": {
@ -180,7 +180,7 @@ fn merge(merge: &Merge, nid: &NodeId, alias: Option<String>) -> Value {
}
/// Returns JSON for a patch `Review` and fills in `alias` when present.
fn review(nid: &NodeId, alias: Option<String>, review: &Review) -> Value {
fn review(nid: &NodeId, alias: Option<Alias>, review: &Review) -> Value {
match alias {
Some(alias) => json!({
"author": {

View File

@ -16,9 +16,11 @@ use radicle::crypto::ssh::keystore::MemorySigner;
use radicle::crypto::ssh::Keystore;
use radicle::crypto::{KeyPair, Seed, Signer};
use radicle::git::{raw as git2, RefString};
use radicle::node;
use radicle::node::address as AddressStore;
use radicle::node::routing as RoutingStore;
use radicle::node::tracking::store as TrackingStore;
use radicle::profile;
use radicle::profile::Home;
use radicle::storage::ReadStorage;
use radicle::Storage;
@ -63,6 +65,9 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
storage,
keystore,
public_key: keypair.pk.into(),
config: profile::Config {
node: node::Config::new(node::Alias::new("seed")),
},
}
}

View File

@ -13,7 +13,7 @@ use serde_json as json;
use crate::identity::Id;
use crate::node::NodeId;
use crate::node::{Command, CommandName, CommandResult};
use crate::node::{Alias, Command, CommandName, CommandResult};
use crate::runtime;
use crate::runtime::thread;
@ -150,6 +150,12 @@ where
[node, alias] => (node.as_str(), Some(alias.to_owned())),
_ => return Err(CommandError::InvalidCommandArgs(cmd.args)),
};
let alias = alias
.map(|a| {
Alias::from_str(&a)
.map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))
})
.transpose()?;
let nid = node
.parse()
.map_err(|e| CommandError::InvalidCommandArg(node.to_owned(), Box::new(e)))?;
@ -358,12 +364,8 @@ mod tests {
assert!(handle.untrack_repo(proj).unwrap());
assert!(!handle.untrack_repo(proj).unwrap());
assert!(handle
.track_node(peer, Some(String::from("alice")))
.unwrap());
assert!(!handle
.track_node(peer, Some(String::from("alice")))
.unwrap());
assert!(handle.track_node(peer, Some(Alias::new("alice"))).unwrap());
assert!(!handle.track_node(peer, Some(Alias::new("alice"))).unwrap());
assert!(handle.untrack_node(peer).unwrap());
assert!(!handle.untrack_node(peer).unwrap());
}

View File

@ -5,11 +5,11 @@ use crossbeam_channel as chan;
use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;
use radicle::node;
use radicle::prelude::Signer;
use radicle::profile;
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
use radicle_node::prelude::{Address, NodeId};
use radicle_node::service::tracking::{Policy, Scope};
use radicle_node::Runtime;
use radicle_node::{logger, service, signals};
use radicle_term as term;
@ -21,7 +21,6 @@ Usage
Options
--alias <alias> Identify yourself with an alias on the network
--connect <peer> Connect to the given peer address on start
--external-address <address> Publicly accessible address (default 0.0.0.0:8776)
--git-daemon <address> Address to bind git-daemon to (default 0.0.0.0:9418)
@ -34,48 +33,29 @@ Options
#[derive(Debug)]
struct Options {
alias: Option<String>,
connect: Vec<(NodeId, Address)>,
external_addresses: Vec<Address>,
daemon: Option<net::SocketAddr>,
limits: service::config::Limits,
listen: Vec<net::SocketAddr>,
force: bool,
tracking_policy: Policy,
tracking_scope: Scope,
}
impl Options {
fn from_env() -> Result<Self, anyhow::Error> {
fn from_env(config: &mut node::Config) -> Result<Self, anyhow::Error> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_env();
let mut alias = None;
let mut connect = Vec::new();
let mut external_addresses = Vec::new();
let mut limits = service::config::Limits::default();
let mut listen = Vec::new();
let mut daemon = None;
let mut tracking_policy = Policy::default();
let mut tracking_scope = Scope::default();
let mut force = false;
while let Some(arg) = parser.next()? {
match arg {
Long("alias") => {
let name: String = parser.value()?.parse()?;
if name.len() > 32 {
anyhow::bail!("alias '{}' is longer than 32 characters", name);
}
alias = Some(name);
}
Long("connect") => {
let peer: PeerAddr<NodeId, Address> = parser.value()?.parse()?;
connect.push((peer.id, peer.addr.clone()));
config.connect.push(peer.into());
}
Long("external-address") => {
let addr = parser.value()?.parse()?;
external_addresses.push(addr);
config.external_addresses.push(addr);
}
Long("force") => {
force = true;
@ -89,24 +69,24 @@ impl Options {
.value()?
.parse()
.map_err(|s| anyhow!("unknown tracking policy {:?}", s))?;
tracking_policy = policy;
config.policy = policy;
}
Long("tracking-scope") => {
let scope = parser
.value()?
.parse()
.map_err(|s| anyhow!("unknown tracking scope {:?}", s))?;
tracking_scope = scope;
config.scope = scope;
}
Long("limit-routing-max-age") => {
let secs: u64 = parser.value()?.parse()?;
limits.routing_max_age = LocalDuration::from_secs(secs);
config.limits.routing_max_age = LocalDuration::from_secs(secs);
}
Long("limit-routing-max-size") => {
limits.routing_max_size = parser.value()?.parse()?;
config.limits.routing_max_size = parser.value()?.parse()?;
}
Long("limit-fetch-concurrency") => {
limits.fetch_concurrency = parser.value()?.parse()?;
config.limits.fetch_concurrency = parser.value()?.parse()?;
}
Long("listen") => {
let addr = parser.value()?.parse()?;
@ -120,7 +100,7 @@ impl Options {
}
}
if external_addresses.len() > service::ADDRESS_LIMIT {
if config.external_addresses.len() > service::ADDRESS_LIMIT {
anyhow::bail!(
"external address limit ({}) exceeded",
service::ADDRESS_LIMIT,
@ -128,15 +108,9 @@ impl Options {
}
Ok(Self {
alias,
connect,
daemon,
external_addresses,
force,
limits,
listen,
tracking_policy,
tracking_scope,
})
}
}
@ -144,13 +118,14 @@ impl Options {
fn execute() -> anyhow::Result<()> {
logger::init(log::Level::Debug)?;
let options = Options::from_env()?;
log::info!(target: "node", "Starting node..");
log::info!(target: "node", "Version {} ({})", env!("CARGO_PKG_VERSION"), env!("GIT_HEAD"));
log::info!(target: "node", "Unlocking node keystore..");
let home = profile::home()?;
let mut config = profile::Config::load(&home.config())?.node;
log::info!(target: "node", "Unlocking node keystore..");
let passphrase = term::io::passphrase(profile::env::RAD_PASSPHRASE)
.context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?;
let keystore = Keystore::new(&home.keys());
@ -158,15 +133,7 @@ fn execute() -> anyhow::Result<()> {
log::info!(target: "node", "Node ID is {}", signer.public_key());
let config = service::Config {
alias: options.alias,
connect: options.connect.into_iter().collect(),
external_addresses: options.external_addresses,
limits: options.limits,
policy: options.tracking_policy,
scope: options.tracking_scope,
..service::Config::default()
};
let options = Options::from_env(&mut config)?;
let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
let daemon = options.daemon.unwrap_or_else(|| {
net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), radicle::git::PROTOCOL_PORT)

View File

@ -154,7 +154,10 @@ impl Runtime {
.ok()
.and_then(|ann| NodeAnnouncement::decode(&mut ann.as_slice()).ok())
.and_then(|ann| {
if config.matches(&ann) {
if config.features() == ann.features
&& config.alias == ann.alias
&& config.external_addresses == ann.addresses.as_ref()
{
Some(ann)
} else {
None
@ -168,8 +171,7 @@ impl Runtime {
);
ann
} else {
config
.node(clock.as_secs())
service::gossip::node(&config, clock.as_secs())
.solve(Default::default())
.expect("Runtime::init: unable to solve proof-of-work puzzle")
};

View File

@ -9,7 +9,7 @@ use reactor::poller::popol::PopolWaker;
use thiserror::Error;
use crate::identity::Id;
use crate::node::{Command, FetchResult};
use crate::node::{Alias, Command, FetchResult};
use crate::profile::Home;
use crate::runtime::Emitter;
use crate::service;
@ -161,7 +161,7 @@ impl radicle::node::Handle for Handle {
receiver.recv().map_err(Error::from)
}
fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error> {
fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
let (sender, receiver) = chan::bounded(1);
self.command(service::Command::TrackNode(id, alias, sender))?;
receiver.recv().map_err(Error::from)

View File

@ -1,7 +1,6 @@
#![allow(clippy::too_many_arguments)]
#![allow(clippy::collapsible_match)]
#![allow(clippy::collapsible_if)]
pub mod config;
pub mod filter;
pub mod io;
pub mod message;
@ -28,7 +27,7 @@ use crate::identity::IdentityError;
use crate::identity::{Doc, Id};
use crate::node::routing;
use crate::node::routing::InsertResult;
use crate::node::{Address, Features, FetchResult, Seed, Seeds};
use crate::node::{Address, Alias, Features, FetchResult, Seed, Seeds};
use crate::prelude::*;
use crate::runtime::Emitter;
use crate::service::message::{Announcement, AnnouncementMessage, Ping};
@ -41,8 +40,7 @@ use crate::worker::FetchError;
use crate::Link;
pub use crate::node::events::{Event, Events};
pub use crate::node::NodeId;
pub use crate::service::config::{Config, Network};
pub use crate::node::{config::Network, Config, NodeId};
pub use crate::service::message::{Message, ZeroBytes};
pub use crate::service::session::Session;
@ -137,7 +135,7 @@ pub enum Command {
/// Untrack the given repository.
UntrackRepo(Id, chan::Sender<bool>),
/// Track the given node.
TrackNode(NodeId, Option<String>, chan::Sender<bool>),
TrackNode(NodeId, Option<Alias>, chan::Sender<bool>),
/// Untrack the given node.
UntrackNode(NodeId, chan::Sender<bool>),
/// Query the internal service state.
@ -382,7 +380,7 @@ where
// Connect to configured peers.
let addrs = self.config.connect.clone();
for (id, addr) in addrs {
for (id, addr) in addrs.into_iter().map(|ca| ca.into()) {
self.connect(id, addr);
}
// Ensure that our inventory is recorded in our routing table, and we are tracking
@ -405,7 +403,7 @@ where
.insert(
&self.node_id(),
self.node.features,
self.node.alias().unwrap_or_default(),
self.node.alias.clone(),
self.node.work(),
self.node.timestamp,
self.node
@ -998,14 +996,6 @@ where
return Ok(false);
}
let alias = match ann.alias() {
Ok(s) => s,
Err(e) => {
warn!(target: "service", "Dropping node announcement from {announcer}: invalid alias: {e}");
return Ok(false);
}
};
// If this node isn't a seed, we're not interested in adding it
// to our address book, but other nodes may be, so we relay the message anyway.
if !features.has(Features::SEED) {
@ -1015,7 +1005,7 @@ where
match self.addresses.insert(
announcer,
*features,
alias,
ann.alias.clone(),
ann.work(),
timestamp,
addresses
@ -1781,7 +1771,7 @@ impl DerefMut for Sessions {
}
}
mod gossip {
pub mod gossip {
use super::*;
use crate::service::filter::Filter;
@ -1842,6 +1832,24 @@ mod gossip {
]
}
pub fn node(config: &Config, timestamp: Timestamp) -> NodeAnnouncement {
let features = config.features();
let alias = config.alias.clone();
let addresses: BoundedVec<_, ADDRESS_LIMIT> = config
.external_addresses
.clone()
.try_into()
.expect("external addresses are within the limit");
NodeAnnouncement {
features,
timestamp,
alias,
addresses,
nonce: 0,
}
}
pub fn inventory(timestamp: Timestamp, inventory: Vec<Id>) -> InventoryAnnouncement {
type Inventory = BoundedVec<Id, INVENTORY_LIMIT>;

View File

@ -1,147 +0,0 @@
use localtime::LocalDuration;
use radicle::node;
use radicle::node::Address;
use crate::bounded::BoundedVec;
use crate::service::message::{NodeAnnouncement, ADDRESS_LIMIT};
use crate::service::tracking::{Policy, Scope};
use crate::service::NodeId;
/// Peer-to-peer network.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub enum Network {
#[default]
Main,
Test,
}
/// Configuration parameters defining attributes of minima and maxima.
#[derive(Debug, Clone)]
pub struct Limits {
/// Number of routing table entries before we start pruning.
pub routing_max_size: usize,
/// How long to keep a routing table entry before being pruned.
pub routing_max_age: LocalDuration,
/// Maximum number of concurrent fetches per per connection.
pub fetch_concurrency: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
routing_max_size: 1000,
routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
fetch_concurrency: 1,
}
}
}
/// Service configuration.
#[derive(Debug, Clone)]
pub struct Config {
/// Alias chosen by the operator.
/// Doesn't have to be unique on the network.
pub alias: Option<String>,
/// Peers to connect to on startup.
/// Connections to these peers will be maintained.
pub connect: Vec<(NodeId, Address)>,
/// Specify the node's public addresses
pub external_addresses: Vec<Address>,
/// Peer-to-peer network.
pub network: Network,
/// Whether or not our node should relay inventories.
pub relay: bool,
/// Configured service limits.
pub limits: Limits,
/// Default tracking policy.
pub policy: Policy,
/// Default tracking scope.
pub scope: Scope,
}
impl Default for Config {
fn default() -> Self {
Self {
alias: None,
connect: Vec::default(),
external_addresses: vec![],
network: Network::default(),
relay: true,
limits: Limits::default(),
policy: Policy::default(),
scope: Scope::default(),
}
}
}
impl Config {
pub fn new(network: Network) -> Self {
Self {
network,
..Self::default()
}
}
pub fn peer(&self, id: &NodeId) -> Option<&Address> {
self.connect.iter().find(|(i, _)| i == id).map(|(_, a)| a)
}
pub fn is_persistent(&self, id: &NodeId) -> bool {
self.connect.iter().any(|(i, _)| i == id)
}
pub fn features(&self) -> node::Features {
node::Features::SEED
}
/// Check if a node announcement matches this configuration.
pub fn matches(&self, other: &NodeAnnouncement) -> bool {
let ann = self.node(other.timestamp);
ann.features == other.features
&& ann.alias == other.alias
&& ann.addresses == other.addresses
}
pub fn alias(&self) -> [u8; 32] {
let mut alias = [0u8; 32];
if let Some(name) = &self.alias {
alias[..name.len()].copy_from_slice(name.as_bytes());
}
alias
}
pub fn node(&self, timestamp: node::Timestamp) -> NodeAnnouncement {
let features = self.features();
let alias = self.alias();
let addresses: BoundedVec<_, ADDRESS_LIMIT> = self
.external_addresses
.clone()
.try_into()
.expect("external addresses are within the limit");
NodeAnnouncement {
features,
timestamp,
alias,
addresses,
nonce: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_announcement() {
let cfg = Config {
alias: Some(String::from("cloudhead")),
..Config::default()
};
assert_eq!("cloudhead", cfg.node(0).alias().unwrap());
}
}

View File

@ -1,10 +1,10 @@
use std::{fmt, io, mem, str};
use std::{fmt, io, mem};
use crate::crypto;
use crate::crypto::Unverified;
use crate::identity::Id;
use crate::node;
use crate::node::Address;
use crate::node::{Address, Alias};
use crate::prelude::BoundedVec;
use crate::service::filter::Filter;
use crate::service::{Link, NodeId, Timestamp};
@ -57,8 +57,8 @@ pub struct NodeAnnouncement {
pub features: node::Features,
/// Monotonic timestamp.
pub timestamp: Timestamp,
/// Non-unique alias. Must be valid UTF-8.
pub alias: [u8; 32],
/// Non-unique alias.
pub alias: Alias,
/// Announced addresses.
pub addresses: BoundedVec<Address, ADDRESS_LIMIT>,
/// Nonce used for announcement proof-of-work.
@ -118,11 +118,6 @@ impl NodeAnnouncement {
}
Some(self)
}
/// Get the alias as a UTF-8 string.
pub fn alias(&self) -> Result<&str, std::str::Utf8Error> {
Ok(str::from_utf8(&self.alias)?.trim_end_matches(0 as char))
}
}
impl wire::Encode for NodeAnnouncement {
@ -619,7 +614,7 @@ mod tests {
let ann = NodeAnnouncement {
features: node::Features::SEED,
timestamp: 42491841,
alias: [0; 32],
alias: Alias::new("alice"),
addresses: BoundedVec::new(),
nonce: 0,
};
@ -627,6 +622,6 @@ mod tests {
assert_eq!(ann.work(), 0);
assert_eq!(ann.clone().solve(1).unwrap().work(), 4);
assert_eq!(ann.clone().solve(8).unwrap().work(), 9);
assert_eq!(ann.solve(14).unwrap().work(), 16);
assert_eq!(ann.solve(14).unwrap().work(), 14);
}
}

View File

@ -1,7 +1,7 @@
use std::collections::{HashSet, VecDeque};
use std::fmt;
use crate::service::config::Limits;
use crate::node::config::Limits;
use crate::service::message;
use crate::service::message::Message;
use crate::service::{Address, Id, LocalTime, NodeId, Outbox, Rng};

View File

@ -2,6 +2,7 @@ use bloomy::BloomFilter;
use qcheck::Arbitrary;
use crate::crypto;
use crate::node::Alias;
use crate::prelude::{BoundedVec, Id, NodeId, Timestamp};
use crate::service::filter::{Filter, FILTER_SIZE_L, FILTER_SIZE_M, FILTER_SIZE_S};
use crate::service::message::{
@ -65,7 +66,7 @@ impl Arbitrary for Message {
let message = NodeAnnouncement {
features: u64::arbitrary(g).into(),
timestamp: Timestamp::arbitrary(g),
alias: <[u8; 32]>::arbitrary(g),
alias: Alias::arbitrary(g),
addresses: Arbitrary::arbitrary(g),
nonce: u64::arbitrary(g),
}

View File

@ -1,6 +1,7 @@
use std::io::BufRead as _;
use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{
collections::{BTreeMap, BTreeSet},
env, fs, io, iter, net, process, thread, time,
@ -21,7 +22,8 @@ use radicle::node::address::Book;
use radicle::node::routing::Store;
use radicle::node::tracking::store as TrackingStore;
use radicle::node::Handle as _;
use radicle::node::{ADDRESS_DB_FILE, TRACKING_DB_FILE};
use radicle::node::{Alias, ADDRESS_DB_FILE, TRACKING_DB_FILE};
use radicle::profile;
use radicle::profile::Home;
use radicle::profile::Profile;
use radicle::rad;
@ -74,8 +76,8 @@ impl Environment {
/// Create a new node in this environment. This should be used when a running node
/// is required. Use [`Environment::profile`] otherwise.
pub fn node(&mut self, name: &str) -> Node<MemorySigner> {
let profile = self.profile(name);
pub fn node(&mut self, config: Config) -> Node<MemorySigner> {
let profile = self.profile(&config.alias);
let signer = MemorySigner::load(&profile.keystore, "radicle".to_owned().into()).unwrap();
let tracking_db = profile.home.node().join(TRACKING_DB_FILE);
TrackingStore::Config::open(tracking_db).unwrap();
@ -85,6 +87,7 @@ impl Environment {
Node {
id: *profile.id(),
home: profile.home,
config,
signer,
storage: profile.storage,
}
@ -92,12 +95,15 @@ impl Environment {
/// Create a new profile in this environment.
/// This should be used when a running node is not required.
pub fn profile(&mut self, name: &str) -> Profile {
let home = Home::new(self.tmp().join("home").join(name).join(".radicle")).unwrap();
pub fn profile(&mut self, alias: &str) -> Profile {
let home = Home::new(self.tmp().join("home").join(alias).join(".radicle")).unwrap();
let storage = Storage::open(home.storage()).unwrap();
let keystore = Keystore::new(&home.keys());
let keypair = KeyPair::from_seed(Seed::from([!(self.users as u8); 32]));
let tracking_db = home.node().join(TRACKING_DB_FILE);
let alias = Alias::from_str(alias).unwrap();
let config = profile::Config::init(alias, &home.config()).unwrap();
TrackingStore::Config::open(tracking_db).unwrap();
let addresses_db = home.node().join(ADDRESS_DB_FILE);
Book::open(addresses_db).unwrap();
@ -115,6 +121,7 @@ impl Environment {
storage,
keystore,
public_key: keypair.pk.into(),
config,
}
}
}
@ -125,6 +132,7 @@ pub struct Node<G> {
pub home: Home,
pub signer: G,
pub storage: Storage,
pub config: Config,
}
/// Handle to a running node.
@ -303,7 +311,7 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
impl Node<MockSigner> {
/// Create a new node.
pub fn init(base: &Path) -> Self {
pub fn init(base: &Path, config: Config) -> Self {
let home = base.join(
iter::repeat_with(fastrand::alphanumeric)
.take(8)
@ -318,13 +326,14 @@ impl Node<MockSigner> {
home,
signer,
storage,
config,
}
}
}
impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
/// Spawn a node in its own thread.
pub fn spawn(self, config: service::Config) -> NodeHandle<G> {
pub fn spawn(self) -> NodeHandle<G> {
let listen = vec![([0, 0, 0, 0], 0).into()];
let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
let daemon: net::SocketAddr = {
@ -337,7 +346,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
let (_, signals) = chan::bounded(1);
let rt = Runtime::init(
self.home.clone(),
config,
self.config,
listen,
proxy,
daemon,

View File

@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use std::{io, time};
use crate::identity::Id;
use crate::node::{Event, FetchResult, Seeds};
use crate::node::{Alias, Event, FetchResult, Seeds};
use crate::runtime::HandleError;
use crate::service::tracking;
use crate::service::NodeId;
@ -51,7 +51,7 @@ impl radicle::node::Handle for Handle {
Ok(self.tracking_repos.lock().unwrap().remove(&id))
}
fn track_node(&mut self, id: NodeId, _alias: Option<String>) -> Result<bool, Self::Error> {
fn track_node(&mut self, id: NodeId, _alias: Option<Alias>) -> Result<bool, Self::Error> {
Ok(self.tracking_nodes.lock().unwrap().insert(id))
}

View File

@ -2,11 +2,12 @@
use std::iter;
use std::net;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use log::*;
use radicle::node::address;
use radicle::node::address::Store;
use radicle::node::{address, Alias};
use radicle::rad;
use radicle::storage::ReadRepository;
use radicle::Storage;
@ -110,7 +111,7 @@ impl Default for Config<MockSigner> {
let signer = MockSigner::new(&mut rng);
Config {
config: service::Config::default(),
config: service::Config::new(Alias::from_str("mocky").unwrap()),
addrs: address::Book::memory().unwrap(),
local_time: LocalTime::now(),
policy: Policy::default(),
@ -162,7 +163,7 @@ where
// Make sure the peer address is advertized.
config.config.external_addresses.push(local_addr.into());
let announcement = config.config.node(config.local_time.as_secs());
let announcement = service::gossip::node(&config.config, config.local_time.as_secs());
let emitter: Emitter<Event> = Default::default();
let service = Service::new(
config.config,
@ -214,7 +215,7 @@ where
.insert(
&peer.node_id(),
radicle::node::Features::default(),
peer.name,
Alias::from_str(peer.name).unwrap(),
0,
timestamp,
Some(known_address),
@ -258,14 +259,11 @@ where
}
pub fn node_announcement(&self) -> Message {
let mut alias = [0u8; 32];
alias[..self.name.len()].copy_from_slice(self.name.as_bytes());
Message::node(
NodeAnnouncement {
features: node::Features::SEED,
timestamp: self.timestamp(),
alias,
alias: Alias::from_str(self.name).unwrap(),
addresses: Some(net::SocketAddr::from((self.ip, node::DEFAULT_PORT)).into()).into(),
nonce: 0,
}

View File

@ -14,9 +14,9 @@ use crate::collections::{HashMap, HashSet};
use crate::crypto::test::signer::MockSigner;
use crate::identity::Id;
use crate::node;
use crate::node::config::*;
use crate::prelude::*;
use crate::prelude::{LocalDuration, Timestamp};
use crate::service::config::*;
use crate::service::filter::Filter;
use crate::service::io::Io;
use crate::service::message::*;
@ -208,8 +208,11 @@ fn test_persistent_peer_connect() {
MockStorage::empty(),
peer::Config {
config: Config {
connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())],
..Config::default()
connect: vec![
(bob.id(), bob.address()).into(),
(eve.id(), eve.address()).into(),
],
..Config::new(node::Alias::new("alice"))
},
..peer::Config::default()
},
@ -320,7 +323,7 @@ fn test_inventory_pruning() {
peer::Config {
config: Config {
limits: test.limits,
..Config::default()
..Config::new(node::Alias::new("alice"))
},
..peer::Config::default()
},
@ -753,7 +756,11 @@ fn test_refs_announcement_trusted() {
// Alice starts to track Bob.
let (sender, receiver) = chan::bounded(1);
alice.command(Command::TrackNode(bob.id, Some("bob".to_string()), sender));
alice.command(Command::TrackNode(
bob.id,
Some(node::Alias::new("bob")),
sender,
));
let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap();
assert!(policy_change);
@ -886,8 +893,11 @@ fn test_persistent_peer_reconnect_attempt() {
MockStorage::empty(),
peer::Config {
config: Config {
connect: vec![(bob.id(), bob.address()), (eve.id(), eve.address())],
..Config::default()
connect: vec![
(bob.id(), bob.address()).into(),
(eve.id(), eve.address()).into(),
],
..Config::new(node::Alias::new("alice"))
},
..peer::Config::default()
},
@ -944,8 +954,8 @@ fn test_persistent_peer_reconnect_success() {
MockStorage::empty(),
peer::Config {
config: Config {
connect: vec![(bob.id, bob.addr())],
..Config::default()
connect: vec![(bob.id, bob.addr()).into()],
..Config::new(node::Alias::new("alice"))
},
..peer::Config::default()
},

View File

@ -2,13 +2,14 @@ use std::{collections::HashSet, thread, time};
use radicle::crypto::{test::signer::MockSigner, Signer};
use radicle::git;
use radicle::node::{FetchResult, Handle as _};
use radicle::node::{Alias, FetchResult, Handle as _};
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository, WriteStorage};
use radicle::test::fixtures;
use radicle::{assert_matches, rad};
use crate::node::config::Limits;
use crate::node::Config;
use crate::service;
use crate::service::config::Limits;
use crate::service::tracking::Scope;
use crate::storage::git::transport;
use crate::test::environment::{converge, Environment, Node};
@ -23,14 +24,14 @@ fn test_inventory_sync_basic() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
alice.project("alice", "");
bob.project("bob", "");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
@ -47,17 +48,17 @@ fn test_inventory_sync_bridge() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let mut eve = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
alice.project("alice", "");
bob.project("bob", "");
eve.project("eve", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let eve = eve.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let eve = eve.spawn();
alice.connect(&bob);
bob.connect(&eve);
@ -77,20 +78,20 @@ fn test_inventory_sync_ring() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let mut eve = Node::init(tmp.path());
let mut carol = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol")));
alice.project("alice", "");
bob.project("bob", "");
eve.project("eve", "");
carol.project("carol", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut eve = eve.spawn(service::Config::default());
let mut carol = carol.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
let mut carol = carol.spawn();
alice.connect(&bob);
bob.connect(&eve);
@ -114,11 +115,11 @@ fn test_inventory_sync_star() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let mut eve = Node::init(tmp.path());
let mut carol = Node::init(tmp.path());
let mut dave = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let mut eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
let mut carol = Node::init(tmp.path(), Config::new(Alias::new("carol")));
let mut dave = Node::init(tmp.path(), Config::new(Alias::new("dave")));
alice.project("alice", "");
bob.project("bob", "");
@ -126,11 +127,11 @@ fn test_inventory_sync_star() {
carol.project("carol", "");
dave.project("dave", "");
let alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut eve = eve.spawn(service::Config::default());
let mut carol = carol.spawn(service::Config::default());
let mut dave = dave.spawn(service::Config::default());
let alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
let mut carol = carol.spawn();
let mut dave = dave.spawn();
bob.connect(&alice);
eve.connect(&alice);
@ -146,12 +147,12 @@ fn test_replication() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = bob.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -203,8 +204,8 @@ fn test_replication_no_delegates() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = bob.project("acme", "");
// Delete one of the signed refs.
@ -216,8 +217,8 @@ fn test_replication_no_delegates() {
.delete()
.unwrap();
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -236,8 +237,8 @@ fn test_replication_no_delegates() {
#[test]
fn test_replication_invalid() {
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let carol = MockSigner::default();
let acme = bob.project("acme", "");
let repo = bob.storage.repository_mut(acme).unwrap();
@ -262,8 +263,8 @@ fn test_replication_invalid() {
)
.unwrap();
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -289,12 +290,12 @@ fn test_migrated_clone() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = alice.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -339,12 +340,12 @@ fn test_dont_fetch_owned_refs() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = alice.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -366,8 +367,8 @@ fn test_fetch_trusted_remotes() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = alice.project("acme", "");
let mut signers = Vec::with_capacity(5);
{
@ -378,8 +379,8 @@ fn test_fetch_trusted_remotes() {
}
}
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -421,12 +422,12 @@ fn test_missing_remote() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = alice.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let carol = MockSigner::default();
alice.connect(&bob);
@ -450,11 +451,11 @@ fn test_fetch_preserve_owned_refs() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = alice.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -496,12 +497,12 @@ fn test_clone() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = bob.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -554,12 +555,12 @@ fn test_fetch_up_to_date() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let acme = bob.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
alice.connect(&bob);
converge([&alice, &bob]);
@ -584,8 +585,8 @@ fn test_large_fetch() {
let env = Environment::new();
let scale = env.scale();
let mut alice = Node::init(&env.tmp());
let bob = Node::init(&env.tmp());
let mut alice = Node::init(&env.tmp(), Config::new(Alias::new("alice")));
let bob = Node::init(&env.tmp(), Config::new(Alias::new("bob")));
let tmp = tempfile::tempdir().unwrap();
let (repo, _) = fixtures::repository(tmp.path());
@ -593,8 +594,8 @@ fn test_large_fetch() {
let rid = alice.project_from("acme", "", &repo);
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let bob_events = bob.handle.events();
bob.handle.track_repo(rid, Scope::All).unwrap();
@ -619,11 +620,28 @@ fn test_concurrent_fetches() {
let env = Environment::new();
let scale = env.scale();
let repos = scale.max(4);
let limits = Limits {
// Have one fetch be queued.
fetch_concurrency: repos - 1,
..Limits::default()
};
let mut bob_repos = HashSet::new();
let mut alice_repos = HashSet::new();
let mut alice = Node::init(&env.tmp());
let mut bob = Node::init(&env.tmp());
let repos = scale.max(4);
let mut alice = Node::init(
&env.tmp(),
service::Config {
limits: limits.clone(),
..service::Config::new(Alias::new("alice"))
},
);
let mut bob = Node::init(
&env.tmp(),
service::Config {
limits,
..service::Config::new(Alias::new("bob"))
},
);
for i in 0..repos {
// Create a repo for Alice.
@ -643,16 +661,8 @@ fn test_concurrent_fetches() {
bob_repos.insert(rid);
}
let config = service::Config {
limits: Limits {
// Have one fetch be queued.
fetch_concurrency: repos - 1,
..Limits::default()
},
..service::Config::default()
};
let mut alice = alice.spawn(config.clone());
let mut bob = bob.spawn(config);
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let alice_events = alice.handle.events();
let bob_events = bob.handle.events();
@ -718,11 +728,11 @@ fn test_connection_crossing() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
alice.handle.connect(bob.id, bob.addr.into()).unwrap();
bob.handle.connect(alice.id, alice.addr.into()).unwrap();
@ -753,15 +763,15 @@ fn test_non_fastforward_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
let alice = Node::init(tmp.path());
let mut bob = Node::init(tmp.path());
let eve = Node::init(tmp.path());
let alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let mut bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
let rid = bob.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
let mut eve = eve.spawn(service::Config::default());
let mut alice = alice.spawn();
let bob = bob.spawn();
let mut eve = eve.spawn();
alice.handle.track_repo(rid, Scope::All).unwrap();
eve.handle.track_repo(rid, Scope::All).unwrap();
@ -805,15 +815,15 @@ fn test_outdated_sigrefs() {
let tmp = tempfile::tempdir().unwrap();
let mut alice = Node::init(tmp.path());
let bob = Node::init(tmp.path());
let eve = Node::init(tmp.path());
let mut alice = Node::init(tmp.path(), Config::new(Alias::new("alice")));
let bob = Node::init(tmp.path(), Config::new(Alias::new("bob")));
let eve = Node::init(tmp.path(), Config::new(Alias::new("eve")));
let rid = alice.project("acme", "");
let mut alice = alice.spawn(service::Config::default());
let mut bob = bob.spawn(service::Config::default());
let mut eve = eve.spawn(service::Config::default());
let mut alice = alice.spawn();
let mut bob = bob.spawn();
let mut eve = eve.spawn();
bob.handle.track_repo(rid, Scope::All).unwrap();
eve.handle.track_repo(rid, Scope::All).unwrap();
@ -832,7 +842,7 @@ fn test_outdated_sigrefs() {
alice
.handle
.track_node(eve.id, Some("eve".to_string()))
.track_node(eve.id, Some(Alias::new("eve")))
.unwrap();
alice.handle.fetch(rid, eve.id).unwrap();
let repo = alice.storage.repository(rid).unwrap();
@ -873,7 +883,7 @@ fn test_outdated_sigrefs() {
alice
.handle
.track_node(bob.id, Some("bob".to_string()))
.track_node(bob.id, Some(Alias::new("bob")))
.unwrap();
assert_matches!(
alice.handle.fetch(rid, bob.id).unwrap(),

View File

@ -10,6 +10,7 @@ pub use protocol::{Control, Wire, WireReader, WireSession, WireWriter};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::ops::Deref;
use std::str::FromStr;
use std::string::FromUtf8Error;
use std::{io, mem};
@ -20,6 +21,7 @@ use crate::git;
use crate::git::fmt;
use crate::identity::Id;
use crate::node;
use crate::node::Alias;
use crate::prelude::*;
use crate::service::filter;
use crate::storage::refs::Refs;
@ -47,6 +49,8 @@ pub enum Error {
InvalidStreamKind(u8),
#[error(transparent)]
InvalidRefName(#[from] fmt::Error),
#[error(transparent)]
InvalidAlias(#[from] node::AliasError),
#[error("invalid control message with type `{0}`")]
InvalidControlMessage(u8),
#[error("invalid protocol version header `{0:x?}`")]
@ -219,6 +223,12 @@ impl Encode for Refs {
}
}
impl Encode for Alias {
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
self.as_ref().encode(writer)
}
}
impl<A, B> Encode for (A, B)
where
A: Encode,
@ -284,6 +294,12 @@ impl Decode for git::RefString {
}
}
impl Decode for Alias {
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
String::decode(reader).and_then(|s| Alias::from_str(&s).map_err(Error::from))
}
}
impl<A, B> Decode for (A, B)
where
A: Decode,
@ -573,6 +589,14 @@ mod tests {
);
}
#[test]
fn test_alias() {
assert_eq!(
serialize(&Alias::from_str("hello").unwrap()),
vec![5, b'h', b'e', b'l', b'l', b'o']
);
}
#[test]
fn test_filter_invalid() {
let b = bloomy::BloomFilter::with_size(filter::FILTER_SIZE_M / 3);

View File

@ -30,6 +30,7 @@ pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
answer: StyleSheet::new(),
highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow),
help_message: StyleSheet::new().with_fg(Color::DarkGrey),
default_value: StyleSheet::new().with_fg(Color::LightBlue),
error_message: ErrorMessageRenderConfig::default_colored()
.with_prefix(Styled::new("").with_fg(Color::LightRed)),
..RenderConfig::default_colored()
@ -173,16 +174,18 @@ pub fn abort<D: fmt::Display>(prompt: D) -> bool {
ask(prompt, false)
}
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
pub fn input<S, E>(message: &str, default: Option<S>, help: Option<&str>) -> anyhow::Result<S>
where
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
E: fmt::Debug + fmt::Display,
{
let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
let value = match default {
Some(default) => input.with_default(default).prompt()?,
None => input.prompt()?,
};
let mut input = CustomType::<S>::new(message).with_render_config(*CONFIG);
input.default = default;
input.help_message = help;
let value = input.prompt()?;
Ok(value)
}

View File

@ -23,10 +23,6 @@ path = "../radicle-cli"
name = "rad-init"
path = "src/rad-init.rs"
[[bin]]
name = "rad-auth"
path = "src/rad-auth.rs"
[[bin]]
name = "rad-self"
path = "src/rad-self.rs"

View File

@ -1,15 +0,0 @@
use radicle::profile;
use radicle::profile::{Error, Profile};
fn main() -> anyhow::Result<()> {
let profile = match Profile::load() {
Ok(profile) => profile,
Err(Error::NotFound(_)) => Profile::init(profile::home()?, "radicle".to_owned())?,
Err(err) => anyhow::bail!(err),
};
println!("id: {}", profile.id());
println!("home: {}", profile.home().display());
Ok(())
}

View File

@ -31,7 +31,7 @@ pub mod prelude {
pub use crypto::{PublicKey, Signer, Verified};
pub use identity::{project::Project, Did, Doc, Id};
pub use node::{NodeId, Timestamp};
pub use node::{Alias, NodeId, Timestamp};
pub use profile::Profile;
pub use storage::{BranchName, ReadRepository, ReadStorage, WriteRepository, WriteStorage};
}

View File

@ -1,6 +1,7 @@
mod features;
pub mod address;
pub mod config;
pub mod events;
pub mod routing;
pub mod tracking;
@ -10,6 +11,7 @@ use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fmt, io, net, thread, time};
use amplify::WrapperMut;
@ -23,6 +25,7 @@ use crate::crypto::PublicKey;
use crate::identity::Id;
use crate::storage::RefUpdate;
pub use config::Config;
pub use cyphernet::addr::PeerAddr;
pub use events::{Event, Events};
pub use features::Features;
@ -33,6 +36,8 @@ pub const DEFAULT_SOCKET_NAME: &str = "control.sock";
pub const DEFAULT_PORT: u16 = 8776;
/// Default timeout when waiting for the node to respond with data.
pub const DEFAULT_TIMEOUT: time::Duration = time::Duration::from_secs(9);
/// Maximum length in bytes of a node alias.
pub const MAX_ALIAS_LENGTH: usize = 32;
/// Filename of routing table database under the node directory.
pub const ROUTING_DB_FILE: &str = "routing.db";
/// Filename of address database under the node directory.
@ -107,6 +112,90 @@ impl fmt::Display for State {
}
}
/// Node alias.
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Alias(String);
impl Alias {
/// Create a new alias from a string. Panics if the string is not a valid alias.
pub fn new(alias: impl ToString) -> Self {
let alias = alias.to_string();
match Self::from_str(&alias) {
Ok(a) => a,
Err(e) => panic!("Alias::new: {e}"),
}
}
}
impl From<Alias> for String {
fn from(value: Alias) -> Self {
value.0
}
}
impl From<&NodeId> for Alias {
fn from(nid: &NodeId) -> Self {
Alias(nid.to_string())
}
}
impl fmt::Display for Alias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Deref for Alias {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for Alias {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl From<&Alias> for [u8; 32] {
fn from(input: &Alias) -> [u8; 32] {
let mut alias = [0u8; 32];
alias[..input.len()].copy_from_slice(input.as_bytes());
alias
}
}
#[derive(thiserror::Error, Debug)]
pub enum AliasError {
#[error("alias cannot be empty")]
Empty,
#[error("alias cannot be greater than {MAX_ALIAS_LENGTH} bytes")]
MaxBytesExceeded,
#[error("alias cannot contain whitespace or control characters")]
InvalidCharacter,
}
impl FromStr for Alias {
type Err = AliasError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(AliasError::Empty);
}
if s.chars().any(|c| c.is_control() || c.is_whitespace()) {
return Err(AliasError::InvalidCharacter);
}
if s.len() > MAX_ALIAS_LENGTH {
return Err(AliasError::MaxBytesExceeded);
}
Ok(Self(s.to_owned()))
}
}
/// Result of a command, on the node control socket.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status")]
@ -499,7 +588,7 @@ pub trait Handle: Clone + Sync + Send {
/// tracked.
fn track_repo(&mut self, id: Id, scope: tracking::Scope) -> Result<bool, Self::Error>;
/// Start tracking the given node.
fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Self::Error>;
fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Self::Error>;
/// Untrack the given project and delete it from storage.
fn untrack_repo(&mut self, id: Id) -> Result<bool, Self::Error>;
/// Untrack the given node.
@ -671,7 +760,7 @@ impl Handle for Node {
Ok(result)
}
fn track_node(&mut self, id: NodeId, alias: Option<String>) -> Result<bool, Error> {
fn track_node(&mut self, id: NodeId, alias: Option<Alias>) -> Result<bool, Error> {
let id = id.to_human();
let args = if let Some(alias) = alias.as_deref() {
vec![id.as_str(), alias]
@ -787,23 +876,23 @@ impl Handle for Node {
/// A trait for different sources which can potentially return an alias.
pub trait AliasStore {
/// Returns alias of a `NodeId`.
fn alias(&self, nid: &NodeId) -> Option<String>;
fn alias(&self, nid: &NodeId) -> Option<Alias>;
}
impl<T: AliasStore + ?Sized> AliasStore for &T {
fn alias(&self, nid: &NodeId) -> Option<String> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
(*self).alias(nid)
}
}
impl<T: AliasStore + ?Sized> AliasStore for Box<T> {
fn alias(&self, nid: &NodeId) -> Option<String> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
self.deref().alias(nid)
}
}
impl AliasStore for HashMap<NodeId, String> {
fn alias(&self, nid: &NodeId) -> Option<String> {
impl AliasStore for HashMap<NodeId, Alias> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
self.get(nid).map(ToOwned::to_owned)
}
}
@ -816,4 +905,19 @@ mod test {
fn test_command_name_display() {
assert_eq!(CommandName::TrackNode.to_string(), "track-node");
}
#[test]
fn test_alias() {
assert!(Alias::from_str("cloudhead").is_ok());
assert!(Alias::from_str("cloud-head").is_ok());
assert!(Alias::from_str("cl0ud.h3ad$__").is_ok());
assert!(Alias::from_str("©loudhèâd").is_ok());
assert!(Alias::from_str("").is_err());
assert!(Alias::from_str(" ").is_err());
assert!(Alias::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
assert!(Alias::from_str("cloud\0head").is_err());
assert!(Alias::from_str("cloud head").is_err());
assert!(Alias::from_str("cloudhead\n").is_err());
}
}

View File

@ -7,7 +7,7 @@ create table if not exists "nodes" (
-- Node features.
"features" integer not null,
-- Node alias.
"alias" text default null,
"alias" text not null,
--- Node announcement proof-of-work.
"pow" integer default 0,
-- Node announcement timestamp.

View File

@ -1,4 +1,5 @@
use std::path::Path;
use std::str::FromStr;
use std::{fmt, io};
use localtime::LocalTime;
@ -7,9 +8,7 @@ use thiserror::Error;
use crate::node;
use crate::node::address::{KnownAddress, Source};
use crate::node::Address;
use crate::node::AliasStore;
use crate::node::NodeId;
use crate::node::{Address, Alias, AliasError, AliasStore, NodeId};
use crate::prelude::Timestamp;
use crate::sql::transaction;
@ -21,6 +20,8 @@ pub enum Error {
/// I/O error.
#[error("i/o error: {0}")]
Io(#[from] io::Error),
#[error("alias error: {0}")]
InvalidAlias(#[from] AliasError),
/// An Internal error.
#[error("internal error: {0}")]
Internal(#[from] sql::Error),
@ -77,7 +78,7 @@ impl Store for Book {
if let Some(Ok(row)) = stmt.into_iter().next() {
let features = row.read::<node::Features, _>("features");
let alias = row.read::<Option<&str>, _>("alias").map(ToOwned::to_owned);
let alias = Alias::from_str(row.read::<&str, _>("alias"))?;
let timestamp = row.read::<i64, _>("timestamp") as Timestamp;
let pow = row.read::<i64, _>("pow") as u32;
let mut addrs = Vec::new();
@ -130,7 +131,7 @@ impl Store for Book {
&mut self,
node: &NodeId,
features: node::Features,
alias: &str,
alias: Alias,
pow: u32,
timestamp: Timestamp,
addrs: impl IntoIterator<Item = KnownAddress>,
@ -143,15 +144,10 @@ impl Store for Book {
SET features = ?2, alias = ?3, pow = ?4, timestamp = ?5
WHERE timestamp < ?5",
)?;
let alias = if alias.is_empty() {
sql::Value::Null
} else {
sql::Value::String(alias.to_owned())
};
stmt.bind((1, node))?;
stmt.bind((2, features))?;
stmt.bind((3, alias))?;
stmt.bind((3, sql::Value::String(alias.into())))?;
stmt.bind((4, pow as i64))?;
stmt.bind((5, timestamp as i64))?;
stmt.next()?;
@ -263,9 +259,9 @@ impl Store for Book {
impl AliasStore for Book {
/// Retrieve `alias` of given node.
/// Calls `Self::get` under the hood.
fn alias(&self, nid: &NodeId) -> Option<String> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
self.get(nid)
.map(|node| node.and_then(|n| n.alias))
.map(|node| node.map(|n| n.alias))
.unwrap_or(None)
}
}
@ -283,7 +279,7 @@ pub trait Store {
&mut self,
node: &NodeId,
features: node::Features,
alias: &str,
alias: Alias,
pow: u32,
timestamp: Timestamp,
addrs: impl IntoIterator<Item = KnownAddress>,
@ -409,16 +405,16 @@ mod test {
let timestamp = LocalTime::now().as_millis();
cache
.insert(&alice, features, "alice", 16, timestamp, [])
.insert(&alice, features, Alias::new("alice"), 16, timestamp, [])
.unwrap();
let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.alias.as_deref(), Some("alice"));
assert_eq!(node.alias.as_ref(), "alice");
cache
.insert(&alice, features, "", 16, timestamp + 1, [])
.insert(&alice, features, Alias::new("bob"), 16, timestamp + 1, [])
.unwrap();
let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.alias.as_deref(), None);
assert_eq!(node.alias.as_ref(), "bob");
}
#[test]
@ -435,7 +431,14 @@ mod test {
last_attempt: None,
};
let inserted = cache
.insert(&alice, features, "alice", 16, timestamp, [ka.clone()])
.insert(
&alice,
features,
Alias::new("alice"),
16,
timestamp,
[ka.clone()],
)
.unwrap();
assert!(inserted);
@ -444,7 +447,7 @@ mod test {
assert_eq!(node.features, features);
assert_eq!(node.pow, 16);
assert_eq!(node.timestamp, timestamp);
assert_eq!(node.alias.as_deref(), Some("alice"));
assert_eq!(node.alias.as_ref(), "alice");
assert_eq!(node.addrs, vec![ka]);
}
@ -454,6 +457,7 @@ mod test {
let mut cache = Book::memory().unwrap();
let features = node::Features::SEED;
let timestamp = LocalTime::now().as_millis();
let alias = Alias::new("alice");
let ka = KnownAddress {
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
@ -462,12 +466,12 @@ mod test {
last_attempt: None,
};
let inserted = cache
.insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.insert(&alice, features, alias.clone(), 0, timestamp, [ka.clone()])
.unwrap();
assert!(inserted);
let inserted = cache
.insert(&alice, features, "alice", 0, timestamp, [ka])
.insert(&alice, features, alias, 0, timestamp, [ka])
.unwrap();
assert!(!inserted);
@ -480,6 +484,8 @@ mod test {
let mut cache = Book::memory().unwrap();
let timestamp = LocalTime::now().as_millis();
let features = node::Features::SEED;
let alias1 = Alias::new("alice");
let alias2 = Alias::new("~alice~");
let ka = KnownAddress {
addr: net::SocketAddr::from(([4, 4, 4, 4], 8776)).into(),
source: Source::Peer,
@ -488,45 +494,38 @@ mod test {
};
let updated = cache
.insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.insert(&alice, features, alias1, 0, timestamp, [ka.clone()])
.unwrap();
assert!(updated);
let updated = cache
.insert(&alice, features, "~alice~", 0, timestamp, [])
.insert(&alice, features, alias2.clone(), 0, timestamp, [])
.unwrap();
assert!(!updated, "Can't update using the same timestamp");
let updated = cache
.insert(&alice, features, "~alice~", 0, timestamp - 1, [])
.insert(&alice, features, alias2.clone(), 0, timestamp - 1, [])
.unwrap();
assert!(!updated, "Can't update using a smaller timestamp");
let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.alias.as_deref(), Some("alice"));
assert_eq!(node.alias.as_ref(), "alice");
assert_eq!(node.timestamp, timestamp);
assert_eq!(node.pow, 0);
let updated = cache
.insert(&alice, features, "~alice~", 0, timestamp + 1, [])
.insert(&alice, features, alias2.clone(), 0, timestamp + 1, [])
.unwrap();
assert!(updated, "Can update with a larger timestamp");
let updated = cache
.insert(
&alice,
node::Features::NONE,
"~alice~",
1,
timestamp + 2,
[],
)
.insert(&alice, node::Features::NONE, alias2, 1, timestamp + 2, [])
.unwrap();
assert!(updated);
let node = cache.get(&alice).unwrap().unwrap();
assert_eq!(node.features, node::Features::NONE);
assert_eq!(node.alias.as_deref(), Some("~alice~"));
assert_eq!(node.alias.as_ref(), "~alice~");
assert_eq!(node.timestamp, timestamp + 2);
assert_eq!(node.pow, 1);
assert_eq!(node.addrs, vec![ka]);
@ -539,6 +538,8 @@ mod test {
let mut cache = Book::memory().unwrap();
let timestamp = LocalTime::now().as_millis();
let features = node::Features::SEED;
let alice_alias = Alias::new("alice");
let bob_alias = Alias::new("bob");
for addr in [
([4, 4, 4, 4], 8776),
@ -552,10 +553,17 @@ mod test {
last_attempt: None,
};
cache
.insert(&alice, features, "alice", 0, timestamp, [ka.clone()])
.insert(
&alice,
features,
alice_alias.clone(),
0,
timestamp,
[ka.clone()],
)
.unwrap();
cache
.insert(&bob, features, "bob", 0, timestamp, [ka])
.insert(&bob, features, bob_alias.clone(), 0, timestamp, [ka])
.unwrap();
}
assert_eq!(cache.len().unwrap(), 6);
@ -577,6 +585,7 @@ mod test {
let mut expected = Vec::new();
let timestamp = LocalTime::now().as_millis();
let features = node::Features::SEED;
let alias = Alias::new("alice");
for id in ids {
let ip = rng.u32(..);
@ -590,7 +599,7 @@ mod test {
};
expected.push((id, ka.clone()));
cache
.insert(&id, features, "alias", 0, timestamp, [ka])
.insert(&id, features, alias.clone(), 0, timestamp, [ka])
.unwrap();
}

View File

@ -5,7 +5,7 @@ use nonempty::NonEmpty;
use crate::collections::HashMap;
use crate::node;
use crate::node::Address;
use crate::node::{Address, Alias};
use crate::prelude::Timestamp;
/// A map with the ability to randomly select values.
@ -78,7 +78,7 @@ impl<K, V> DerefMut for AddressBook<K, V> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
/// Advertized alias.
pub alias: Option<String>,
pub alias: Alias,
/// Advertized features.
pub features: node::Features,
/// Advertized addresses

126
radicle/src/node/config.rs Normal file
View File

@ -0,0 +1,126 @@
use std::ops::Deref;
use cyphernet::addr::PeerAddr;
use localtime::LocalDuration;
use crate::node;
use crate::node::tracking::{Policy, Scope};
use crate::node::{Address, Alias, NodeId};
/// Peer-to-peer network.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Network {
#[default]
Main,
Test,
}
/// Configuration parameters defining attributes of minima and maxima.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Limits {
/// Number of routing table entries before we start pruning.
pub routing_max_size: usize,
/// How long to keep a routing table entry before being pruned.
#[serde(with = "crate::serde_ext::localtime::duration")]
pub routing_max_age: LocalDuration,
/// Maximum number of concurrent fetches per per connection.
pub fetch_concurrency: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
routing_max_size: 1000,
routing_max_age: LocalDuration::from_mins(7 * 24 * 60),
fetch_concurrency: 1,
}
}
}
/// Full address used to connect to a remote node.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct ConnectAddress(#[serde(with = "crate::serde_ext::string")] PeerAddr<NodeId, Address>);
impl From<PeerAddr<NodeId, Address>> for ConnectAddress {
fn from(value: PeerAddr<NodeId, Address>) -> Self {
Self(value)
}
}
impl From<ConnectAddress> for (NodeId, Address) {
fn from(value: ConnectAddress) -> Self {
(value.0.id, value.0.addr)
}
}
impl From<(NodeId, Address)> for ConnectAddress {
fn from((id, addr): (NodeId, Address)) -> Self {
Self(PeerAddr { id, addr })
}
}
impl Deref for ConnectAddress {
type Target = PeerAddr<NodeId, Address>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Service configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
/// Node alias.
pub alias: Alias,
/// Peers to connect to on startup.
/// Connections to these peers will be maintained.
pub connect: Vec<ConnectAddress>,
/// Specify the node's public addresses
pub external_addresses: Vec<Address>,
/// Peer-to-peer network.
pub network: Network,
/// Whether or not our node should relay inventories.
pub relay: bool,
/// Configured service limits.
pub limits: Limits,
/// Default tracking policy.
pub policy: Policy,
/// Default tracking scope.
pub scope: Scope,
}
impl Config {
pub fn new(alias: Alias) -> Self {
Self {
alias,
connect: Vec::default(),
external_addresses: vec![],
network: Network::default(),
relay: true,
limits: Limits::default(),
policy: Policy::default(),
scope: Scope::default(),
}
}
}
impl Config {
pub fn peer(&self, id: &NodeId) -> Option<&Address> {
self.connect
.iter()
.find(|ca| &ca.id == id)
.map(|ca| &ca.addr)
}
pub fn is_persistent(&self, id: &NodeId) -> bool {
self.peer(id).is_some()
}
pub fn features(&self) -> node::Features {
node::Features::SEED
}
}

View File

@ -8,7 +8,7 @@ use thiserror::Error;
use crate::prelude::Id;
use super::NodeId;
pub use super::{Alias, NodeId};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Repo {
@ -24,11 +24,9 @@ pub struct Node {
pub policy: Policy,
}
/// Node alias.
pub type Alias = String;
/// Tracking policy.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Policy {
/// The resource is tracked.
Track,
@ -91,6 +89,7 @@ impl TryFrom<&sqlite::Value> for Policy {
/// Tracking scope of a repository tracking policy.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Scope {
/// Track remotes of nodes that are already tracked.
#[default]

View File

@ -1,11 +1,11 @@
#![allow(clippy::type_complexity)]
use std::path::Path;
use std::{fmt, io, ops::Not as _, time};
use std::{fmt, io, ops::Not as _, str::FromStr, time};
use sqlite as sql;
use thiserror::Error;
use crate::node::AliasStore;
use crate::node::{Alias, AliasStore};
use crate::prelude::{Id, NodeId};
use super::{Node, Policy, Repo, Scope};
@ -188,7 +188,11 @@ impl Config {
if let Some(Ok(row)) = stmt.into_iter().next() {
let alias = row.read::<&str, _>("alias");
let alias = alias.is_empty().not().then_some(alias.to_owned());
let alias = alias
.is_empty()
.not()
.then_some(alias.to_owned())
.and_then(|s| Alias::from_str(&s).ok());
let policy = row.read::<Policy, _>("policy");
return Ok(Some(Node {
@ -229,7 +233,11 @@ impl Config {
while let Some(Ok(row)) = stmt.next() {
let id = row.read("id");
let alias = row.read::<&str, _>("alias").to_owned();
let alias = alias.is_empty().not().then_some(alias.to_owned());
let alias = alias
.is_empty()
.not()
.then_some(alias.to_owned())
.and_then(|s| Alias::from_str(&s).ok());
let policy = row.read::<Policy, _>("policy");
entries.push(Node { id, alias, policy });
@ -260,7 +268,7 @@ impl Config {
impl AliasStore for Config {
/// Retrieve `alias` of given node.
/// Calls `Self::node_policy` under the hood.
fn alias(&self, nid: &NodeId) -> Option<String> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
self.node_policy(nid)
.map(|node| node.and_then(|n| n.alias))
.unwrap_or(None)
@ -334,7 +342,7 @@ mod test {
assert!(db.track_node(&id, Some("eve")).unwrap());
assert_eq!(
db.node_policy(&id).unwrap().unwrap().alias,
Some(String::from("eve"))
Some(Alias::from_str("eve").unwrap())
);
assert!(db.track_node(&id, None).unwrap());
assert_eq!(db.node_policy(&id).unwrap().unwrap().alias, None);
@ -342,7 +350,7 @@ mod test {
assert!(db.track_node(&id, Some("alice")).unwrap());
assert_eq!(
db.node_policy(&id).unwrap().unwrap().alias,
Some(String::from("alice"))
Some(Alias::new("alice"))
);
}

View File

@ -10,16 +10,18 @@
//! node/
//! control.sock # Node control socket
//!
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};
use std::{fs, io, str::FromStr};
use serde::Serialize;
use thiserror::Error;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
use crate::crypto::{PublicKey, Signer};
use crate::node;
use crate::node::{address, routing, tracking, AliasStore};
use crate::node::{address, routing, tracking, Alias, AliasStore};
use crate::prelude::Did;
use crate::prelude::NodeId;
use crate::storage::git::transport;
@ -49,10 +51,12 @@ pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Keystore(#[from] keystore::Error),
#[error(transparent)]
MemorySigner(#[from] keystore::MemorySignerError),
#[error("no profile found at the filepath '{0}'")]
#[error("no profile found at path '{0}'")]
NotFound(PathBuf),
#[error("error connecting to ssh-agent: {0}")]
Agent(#[from] crate::crypto::ssh::agent::Error),
@ -64,19 +68,86 @@ pub enum Error {
AddressStore(#[from] node::address::Error),
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to load node configuration from {0}: {1}")]
Io(PathBuf, io::Error),
#[error("failed to decode node configuration from {0}: {1}")]
Json(PathBuf, serde_json::Error),
}
/// Local radicle configuration.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub node: node::Config,
}
impl Config {
/// Initialize a new configuration. Fails if the path already exists.
pub fn init(alias: Alias, path: &Path) -> io::Result<Self> {
let cfg = Self {
node: node::Config::new(alias),
};
let mut file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(path)?;
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut serializer = serde_json::Serializer::with_formatter(&file, formatter);
cfg.serialize(&mut serializer)?;
file.write_all(b"\n")?;
file.sync_all()?;
Ok(cfg)
}
/// Load a configuration from the given path.
pub fn load(path: &Path) -> Result<Self, ConfigError> {
match fs::File::open(path) {
Ok(cfg) => {
serde_json::from_reader(cfg).map_err(|e| ConfigError::Json(path.to_path_buf(), e))
}
Err(e) => {
let Ok(user) = env::var("USER") else {
return Err(ConfigError::Io(path.to_owned(), e));
};
let Ok(alias) = Alias::from_str(&user) else {
return Err(ConfigError::Io(path.to_owned(), e));
};
Ok(Config {
node: node::Config::new(alias),
})
}
}
}
/// Get the user alias.
pub fn alias(&self) -> &Alias {
&self.node.alias
}
}
#[derive(Debug, Clone)]
pub struct Profile {
pub home: Home,
pub storage: Storage,
pub keystore: Keystore,
pub public_key: PublicKey,
pub config: Config,
}
impl Profile {
pub fn init(home: Home, passphrase: impl Into<Passphrase>) -> Result<Self, Error> {
pub fn init(
home: Home,
alias: Alias,
passphrase: impl Into<Passphrase>,
) -> Result<Self, Error> {
let storage = Storage::open(home.storage())?;
let keystore = Keystore::new(&home.keys());
let public_key = keystore.init("radicle", passphrase)?;
let config = Config::init(alias, home.config().as_path())?;
transport::local::register(storage.clone());
@ -85,6 +156,7 @@ impl Profile {
storage,
keystore,
public_key,
config,
})
}
@ -95,6 +167,7 @@ impl Profile {
let public_key = keystore
.public_key()?
.ok_or_else(|| Error::NotFound(home.path().to_path_buf()))?;
let config = Config::load(home.config().as_path())?;
transport::local::register(storage.clone());
@ -103,6 +176,7 @@ impl Profile {
storage,
keystore,
public_key,
config,
})
}
@ -194,7 +268,7 @@ pub struct Aliases {
impl AliasStore for Aliases {
/// Retrieve `alias` of given node.
/// First looks in `tracking.db` and then `addresses.db`.
fn alias(&self, nid: &NodeId) -> Option<String> {
fn alias(&self, nid: &NodeId) -> Option<Alias> {
self.tracking
.alias(nid)
.or_else(|| self.addresses.alias(nid))
@ -267,6 +341,10 @@ impl Home {
self.path.join("storage")
}
pub fn config(&self) -> PathBuf {
self.path.join("config.json")
}
pub fn keys(&self) -> PathBuf {
self.path.join("keys")
}

View File

@ -24,28 +24,53 @@ pub mod string {
}
}
/// Unlike the default `serde` instance for `LocalTime`, this encodes and decodes using seconds
/// Unlike the default `serde` instances from `localtime`, this encodes and decodes using seconds
/// instead of milliseconds.
pub mod localtime {
use localtime::LocalTime;
use serde::{de, Deserialize, Deserializer, Serializer};
pub mod time {
use localtime::LocalTime;
use serde::{de, Deserialize, Deserializer, Serializer};
pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&value.as_secs())
pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&value.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
where
D: Deserializer<'de>,
{
let seconds: u64 = String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)?;
Ok(LocalTime::from_secs(seconds))
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
where
D: Deserializer<'de>,
{
let seconds: u64 = String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)?;
pub mod duration {
use localtime::LocalDuration;
use serde::{de, Deserialize, Deserializer, Serializer};
Ok(LocalTime::from_secs(seconds))
pub fn serialize<S>(value: &LocalDuration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&value.as_secs())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalDuration, D::Error>
where
D: Deserializer<'de>,
{
let seconds: u64 = String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)?;
Ok(LocalDuration::from_secs(seconds))
}
}
}

View File

@ -1,6 +1,7 @@
use std::collections::{BTreeMap, HashSet};
use std::hash::Hash;
use std::ops::RangeBounds;
use std::str::FromStr;
use std::{iter, net};
use crypto::test::signer::MockSigner;
@ -14,7 +15,7 @@ use crate::identity::{
project::Project,
Did,
};
use crate::node::Address;
use crate::node::{Address, Alias};
use crate::storage;
use crate::storage::refs::{Refs, SignedRefs};
use crate::test::storage::{MockRepository, MockStorage};
@ -227,3 +228,13 @@ impl Arbitrary for Address {
})
}
}
impl Arbitrary for Alias {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
let s = g
.choose(&["cloudhead", "alice", "bob", "john-lu", "f0_"])
.unwrap();
Alias::from_str(s).unwrap()
}
}