node: Implement auto-fetching of projects
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
a5a7af57b5
commit
92f17d71c3
|
|
@ -180,6 +180,13 @@ impl Config {
|
||||||
self.connect.contains(addr)
|
self.connect.contains(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_tracking(&self, proj: &ProjId) -> bool {
|
||||||
|
match &self.project_tracking {
|
||||||
|
ProjectTracking::All { blocked } => !blocked.contains(proj),
|
||||||
|
ProjectTracking::Allowed(projs) => projs.contains(proj),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Track a project. Returns whether the policy was updated.
|
/// Track a project. Returns whether the policy was updated.
|
||||||
pub fn track(&mut self, proj: ProjId) -> bool {
|
pub fn track(&mut self, proj: ProjId) -> bool {
|
||||||
match &mut self.project_tracking {
|
match &mut self.project_tracking {
|
||||||
|
|
@ -686,7 +693,7 @@ pub struct Context<S, T> {
|
||||||
|
|
||||||
impl<S, T> Context<S, T>
|
impl<S, T> Context<S, T>
|
||||||
where
|
where
|
||||||
T: storage::ReadStorage,
|
T: storage::ReadStorage + storage::WriteStorage,
|
||||||
{
|
{
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|
@ -706,22 +713,25 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current local timestamp.
|
|
||||||
pub(crate) fn timestamp(&self) -> Timestamp {
|
|
||||||
self.clock.local_time().as_secs()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process a peer inventory announcement by updating our routing table.
|
/// Process a peer inventory announcement by updating our routing table.
|
||||||
fn process_inventory(&mut self, inventory: &Inventory, from: PeerId) {
|
fn process_inventory(&mut self, inventory: &Inventory, from: PeerId, remote: &Url) {
|
||||||
for proj_id in inventory {
|
for proj_id in inventory {
|
||||||
let inventory = self
|
let inventory = self
|
||||||
.routing
|
.routing
|
||||||
.entry(proj_id.clone())
|
.entry(proj_id.clone())
|
||||||
.or_insert_with(|| HashSet::with_hasher(self.rng.clone().into()));
|
.or_insert_with(|| HashSet::with_hasher(self.rng.clone().into()));
|
||||||
|
|
||||||
// TODO: If we're tracking this project, check the refs to see if we need to
|
if self.config.is_tracking(proj_id) {
|
||||||
// fetch updates from this peer.
|
// TODO: Verify refs before adding them to storage.
|
||||||
|
let mut repo = self.storage.repository(proj_id).unwrap();
|
||||||
|
repo.fetch(&Url {
|
||||||
|
path: format!("/{}", proj_id).into(),
|
||||||
|
..remote.clone()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fire an event on routing update.
|
||||||
inventory.insert(from);
|
inventory.insert(from);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -733,6 +743,11 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, T> Context<S, T> {
|
impl<S, T> Context<S, T> {
|
||||||
|
/// Get current local timestamp.
|
||||||
|
pub(crate) fn timestamp(&self) -> Timestamp {
|
||||||
|
self.clock.local_time().as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect to a peer.
|
/// Connect to a peer.
|
||||||
fn connect(&mut self, addr: net::SocketAddr) {
|
fn connect(&mut self, addr: net::SocketAddr) {
|
||||||
// TODO: Make sure we don't try to connect more than once to the same address.
|
// TODO: Make sure we don't try to connect more than once to the same address.
|
||||||
|
|
@ -816,6 +831,8 @@ pub enum PeerError {
|
||||||
WrongVersion(u32),
|
WrongVersion(u32),
|
||||||
#[error("invalid inventory timestamp: {0}")]
|
#[error("invalid inventory timestamp: {0}")]
|
||||||
InvalidTimestamp(u64),
|
InvalidTimestamp(u64),
|
||||||
|
#[error("peer misbehaved")]
|
||||||
|
Misbehavior,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -866,19 +883,22 @@ impl Peer {
|
||||||
ctx: &mut Context<S, T>,
|
ctx: &mut Context<S, T>,
|
||||||
) -> Result<Option<Message>, PeerError>
|
) -> Result<Option<Message>, PeerError>
|
||||||
where
|
where
|
||||||
T: storage::ReadStorage,
|
T: storage::ReadStorage + storage::WriteStorage,
|
||||||
{
|
{
|
||||||
if envelope.magic != NETWORK_MAGIC {
|
if envelope.magic != NETWORK_MAGIC {
|
||||||
return Err(PeerError::WrongMagic(envelope.magic));
|
return Err(PeerError::WrongMagic(envelope.magic));
|
||||||
}
|
}
|
||||||
debug!("Received {:?} from {}", &envelope.msg, self.id());
|
debug!("Received {:?} from {}", &envelope.msg, self.id());
|
||||||
|
|
||||||
match envelope.msg {
|
match (&self.state, envelope.msg) {
|
||||||
Message::Hello {
|
(
|
||||||
timestamp,
|
PeerState::Initial,
|
||||||
version,
|
Message::Hello {
|
||||||
git,
|
timestamp,
|
||||||
} => {
|
version,
|
||||||
|
git,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
let now = ctx.timestamp();
|
let now = ctx.timestamp();
|
||||||
|
|
||||||
if timestamp.abs_diff(now) > MAX_TIME_DELTA.as_secs() {
|
if timestamp.abs_diff(now) > MAX_TIME_DELTA.as_secs() {
|
||||||
|
|
@ -887,37 +907,43 @@ impl Peer {
|
||||||
if version != PROTOCOL_VERSION {
|
if version != PROTOCOL_VERSION {
|
||||||
return Err(PeerError::WrongVersion(version));
|
return Err(PeerError::WrongVersion(version));
|
||||||
}
|
}
|
||||||
if let PeerState::Initial = self.state {
|
// Nb. This is a very primitive handshake. Eventually we should have anyhow
|
||||||
// Nb. This is a very primitive handshake. Eventually we should have anyhow
|
// extra "acknowledgment" message sent when the `Hello` is well received.
|
||||||
// extra "acknowledgment" message sent when the `Hello` is well received.
|
if self.link.is_inbound() {
|
||||||
if self.link.is_inbound() {
|
let git = ctx.config.git_url.clone();
|
||||||
let git = ctx.config.git_url.clone();
|
ctx.write_all(
|
||||||
ctx.write_all(
|
self.addr,
|
||||||
self.addr,
|
[Message::hello(now, git), Message::get_inventory([])],
|
||||||
[Message::hello(now, git), Message::get_inventory([])],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
// Nb. we don't set the peer timestamp here, since it is going to be
|
|
||||||
// set after the first message is received only. Setting it here would
|
|
||||||
// mean that messages received right after the handshake could be ignored.
|
|
||||||
self.state = PeerState::Negotiated {
|
|
||||||
since: ctx.clock.local_time(),
|
|
||||||
git,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// TODO: Handle misbehavior.
|
|
||||||
}
|
}
|
||||||
|
// Nb. we don't set the peer timestamp here, since it is going to be
|
||||||
|
// set after the first message is received only. Setting it here would
|
||||||
|
// mean that messages received right after the handshake could be ignored.
|
||||||
|
self.state = PeerState::Negotiated {
|
||||||
|
since: ctx.clock.local_time(),
|
||||||
|
git,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Message::GetInventory { .. } => {
|
(PeerState::Initial, _) => {
|
||||||
|
debug!(
|
||||||
|
"Disconnecting peer {} for sending us a message before handshake",
|
||||||
|
self.id()
|
||||||
|
);
|
||||||
|
return Err(PeerError::Misbehavior);
|
||||||
|
}
|
||||||
|
(PeerState::Negotiated { .. }, Message::GetInventory { .. }) => {
|
||||||
// TODO: Handle partial inventory requests.
|
// TODO: Handle partial inventory requests.
|
||||||
let inventory = Message::inventory(ctx).unwrap();
|
let inventory = Message::inventory(ctx).unwrap();
|
||||||
ctx.write(self.addr, inventory);
|
ctx.write(self.addr, inventory);
|
||||||
}
|
}
|
||||||
Message::Inventory {
|
(
|
||||||
timestamp,
|
PeerState::Negotiated { git, .. },
|
||||||
inv,
|
Message::Inventory {
|
||||||
origin,
|
timestamp,
|
||||||
} => {
|
inv,
|
||||||
|
origin,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
let now = ctx.clock.local_time();
|
let now = ctx.clock.local_time();
|
||||||
let last = self.timestamp;
|
let last = self.timestamp;
|
||||||
|
|
||||||
|
|
@ -932,7 +958,7 @@ impl Peer {
|
||||||
} else {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
ctx.process_inventory(&inv, origin.unwrap_or_else(|| self.id()));
|
ctx.process_inventory(&inv, origin.unwrap_or_else(|| self.id()), git);
|
||||||
|
|
||||||
if ctx.config.relay {
|
if ctx.config.relay {
|
||||||
return Ok(Some(Message::Inventory {
|
return Ok(Some(Message::Inventory {
|
||||||
|
|
@ -942,14 +968,24 @@ impl Peer {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::GetAddrs => {
|
(PeerState::Negotiated { .. }, Message::GetAddrs) => {
|
||||||
// TODO: Send peer addresses.
|
// TODO: Send peer addresses.
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
Message::Addrs { .. } => {
|
(PeerState::Negotiated { .. }, Message::Addrs { .. }) => {
|
||||||
// TODO: Update address book.
|
// TODO: Update address book.
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
(PeerState::Negotiated { .. }, Message::Hello { .. }) => {
|
||||||
|
debug!(
|
||||||
|
"Disconnecting peer {} for sending us a redundant handshake message",
|
||||||
|
self.id()
|
||||||
|
);
|
||||||
|
return Err(PeerError::Misbehavior);
|
||||||
|
}
|
||||||
|
(PeerState::Disconnected { .. }, msg) => {
|
||||||
|
debug!("Ignoring {:?} from disconnected peer {}", msg, self.id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ impl Remote<Unverified> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReadStorage {
|
pub trait ReadStorage {
|
||||||
|
fn url(&self) -> Url;
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error>;
|
||||||
fn inventory(&self) -> Result<Inventory, Error>;
|
fn inventory(&self) -> Result<Inventory, Error>;
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +141,10 @@ where
|
||||||
T: Deref<Target = S>,
|
T: Deref<Target = S>,
|
||||||
S: ReadStorage,
|
S: ReadStorage,
|
||||||
{
|
{
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.deref().url()
|
||||||
|
}
|
||||||
|
|
||||||
fn inventory(&self) -> Result<Inventory, Error> {
|
fn inventory(&self) -> Result<Inventory, Error> {
|
||||||
self.deref().inventory()
|
self.deref().inventory()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ impl fmt::Debug for Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadStorage for Storage {
|
impl ReadStorage for Storage {
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
Url {
|
||||||
|
scheme: git_url::Scheme::File,
|
||||||
|
host: Some(self.path.to_string_lossy().to_string()),
|
||||||
|
..Url::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get(&self, _id: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
fn get(&self, _id: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,10 @@ where
|
||||||
pub fn new(name: &'static str, ip: impl Into<net::IpAddr>, storage: S) -> Self {
|
pub fn new(name: &'static str, ip: impl Into<net::IpAddr>, storage: S) -> Self {
|
||||||
Self::config(
|
Self::config(
|
||||||
name,
|
name,
|
||||||
Config::default(),
|
Config {
|
||||||
|
git_url: storage.url(),
|
||||||
|
..Config::default()
|
||||||
|
},
|
||||||
ip,
|
ip,
|
||||||
vec![],
|
vec![],
|
||||||
storage,
|
storage,
|
||||||
|
|
@ -143,20 +146,22 @@ where
|
||||||
.expect("`get-inventory` is sent");
|
.expect("`get-inventory` is sent");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect_to(&mut self, remote: &net::SocketAddr) {
|
pub fn connect_to(&mut self, peer: &Self) {
|
||||||
self.initialize();
|
let remote = simulator::Peer::<Protocol<S>>::addr(peer);
|
||||||
self.protocol.attempted(remote);
|
|
||||||
self.protocol
|
|
||||||
.connected(*remote, &self.local_addr, Link::Outbound);
|
|
||||||
|
|
||||||
let mut msgs = self.messages(remote);
|
self.initialize();
|
||||||
|
self.protocol.attempted(&remote);
|
||||||
|
self.protocol
|
||||||
|
.connected(remote, &self.local_addr, Link::Outbound);
|
||||||
|
|
||||||
|
let mut msgs = self.messages(&remote);
|
||||||
msgs.find(|m| matches!(m, Message::Hello { .. }))
|
msgs.find(|m| matches!(m, Message::Hello { .. }))
|
||||||
.expect("`hello` is sent");
|
.expect("`hello` is sent");
|
||||||
msgs.find(|m| matches!(m, Message::GetInventory { .. }))
|
msgs.find(|m| matches!(m, Message::GetInventory { .. }))
|
||||||
.expect("`get-inventory` is sent");
|
.expect("`get-inventory` is sent");
|
||||||
|
|
||||||
let git = self.config().git_url.clone();
|
let git = peer.config().git_url.clone();
|
||||||
self.receive(remote, Message::hello(self.local_time().as_secs(), git));
|
self.receive(&remote, Message::hello(self.local_time().as_secs(), git));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drain outgoing messages sent from this peer to the remote address.
|
/// Drain outgoing messages sent from this peer to the remote address.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ impl MockStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadStorage for MockStorage {
|
impl ReadStorage for MockStorage {
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
Url {
|
||||||
|
scheme: git_url::Scheme::Radicle,
|
||||||
|
host: Some("mock".to_string()),
|
||||||
|
..Url::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
fn get(&self, proj: &ProjId) -> Result<Option<Remotes<Unverified>>, Error> {
|
||||||
if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
|
if let Some((_, refs)) = self.inventory.iter().find(|(id, _)| id == proj) {
|
||||||
return Ok(Some(refs.clone()));
|
return Ok(Some(refs.clone()));
|
||||||
|
|
@ -45,7 +53,7 @@ impl WriteStorage for MockStorage {
|
||||||
type Repository = MockRepository;
|
type Repository = MockRepository;
|
||||||
|
|
||||||
fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
|
fn repository(&self, _proj: &ProjId) -> Result<Self::Repository, Error> {
|
||||||
todo!()
|
Ok(MockRepository {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +61,7 @@ pub struct MockRepository {}
|
||||||
|
|
||||||
impl WriteRepository for MockRepository {
|
impl WriteRepository for MockRepository {
|
||||||
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
|
||||||
todo!()
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn namespace(
|
fn namespace(
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ fn test_outbound_connection() {
|
||||||
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
|
let bob = Peer::new("bob", [9, 9, 9, 9], MockStorage::empty());
|
||||||
let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
|
let eve = Peer::new("eve", [7, 7, 7, 7], MockStorage::empty());
|
||||||
|
|
||||||
alice.connect_to(&bob.addr());
|
alice.connect_to(&bob);
|
||||||
alice.connect_to(&eve.addr());
|
alice.connect_to(&eve);
|
||||||
|
|
||||||
let peers = alice
|
let peers = alice
|
||||||
.protocol
|
.protocol
|
||||||
|
|
@ -120,20 +120,42 @@ fn test_wrong_peer_magic() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inventory_fetch() {
|
fn test_inventory_sync() {
|
||||||
let mut alice = Peer::new("alice", [7, 7, 7, 7], MockStorage::empty());
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let bob = Peer::new("bob", [8, 8, 8, 8], MockStorage::empty());
|
let mut alice = Peer::new(
|
||||||
|
"alice",
|
||||||
|
[7, 7, 7, 7],
|
||||||
|
Storage::open(tmp.path().join("alice")).unwrap(),
|
||||||
|
);
|
||||||
|
let bob_storage = fixtures::storage(tmp.path().join("bob"));
|
||||||
|
let bob = Peer::new("bob", [8, 8, 8, 8], bob_storage);
|
||||||
let now = LocalTime::now().as_secs();
|
let now = LocalTime::now().as_secs();
|
||||||
|
let projs = bob.storage().inventory().unwrap();
|
||||||
|
|
||||||
alice.connect_to(&bob.addr());
|
alice.connect_to(&bob);
|
||||||
alice.receive(
|
alice.receive(
|
||||||
&bob.addr(),
|
&bob.addr(),
|
||||||
Message::Inventory {
|
Message::Inventory {
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
inv: vec![],
|
inv: projs.clone(),
|
||||||
origin: None,
|
origin: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for proj in &projs {
|
||||||
|
let providers = alice.routing().get(proj).unwrap();
|
||||||
|
assert!(providers.contains(&bob.ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = alice
|
||||||
|
.storage()
|
||||||
|
.inventory()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let b = projs.into_iter().collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -143,7 +165,7 @@ fn test_inventory_relay_bad_timestamp() {
|
||||||
let two_hours = 3600 * 2;
|
let two_hours = 3600 * 2;
|
||||||
let timestamp = alice.local_time.as_secs() - two_hours;
|
let timestamp = alice.local_time.as_secs() - two_hours;
|
||||||
|
|
||||||
alice.connect_to(&bob.addr());
|
alice.connect_to(&bob);
|
||||||
alice.receive(
|
alice.receive(
|
||||||
&bob.addr(),
|
&bob.addr(),
|
||||||
Message::Inventory {
|
Message::Inventory {
|
||||||
|
|
@ -169,7 +191,7 @@ fn test_inventory_relay() {
|
||||||
let now = LocalTime::now().as_secs();
|
let now = LocalTime::now().as_secs();
|
||||||
|
|
||||||
// Inventory from Bob relayed to Eve.
|
// Inventory from Bob relayed to Eve.
|
||||||
alice.connect_to(&bob.addr());
|
alice.connect_to(&bob);
|
||||||
alice.connect_from(&eve.addr());
|
alice.connect_from(&eve.addr());
|
||||||
alice.receive(
|
alice.receive(
|
||||||
&bob.addr(),
|
&bob.addr(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue