Skip to main content

bitwarden_shared_unlock/
active_peers.rs

1//! Liveness tracking for the peers that sync to this one.
2
3use std::{collections::HashMap, sync::Mutex, time::Duration};
4
5use bitwarden_ipc::{Endpoint, Source};
6use tracing::info;
7use web_time::Instant;
8
9/// A peer to sync to, together with the origin
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub(crate) struct SyncTarget {
12    /// The address to send to.
13    pub(crate) endpoint: Endpoint,
14    /// The origin this peer was validated against. `Some` only for web peers.
15    pub(crate) origin: Option<String>,
16}
17
18impl SyncTarget {
19    /// The target for a validated incoming source, keeping the origin the source carried.
20    pub(crate) fn from_source(source: &Source) -> Self {
21        Self {
22            endpoint: source.to_endpoint(),
23            origin: match source {
24                Source::Web { origin, .. } => Some(origin.clone()),
25                _ => None,
26            },
27        }
28    }
29
30    /// A target with no origin, for a peer that was not reached through a validated source.
31    pub(crate) fn without_origin(endpoint: Endpoint) -> Self {
32        Self {
33            endpoint,
34            origin: None,
35        }
36    }
37}
38
39/// What is known about a peer that syncs to this one.
40struct PeerRecord {
41    /// When the peer was last heard from.
42    last_seen: Instant,
43    /// The origin validated when the peer was registered. `Some` only for web peers.
44    origin: Option<String>,
45}
46
47/// Tracker for the active peers
48#[derive(Default)]
49pub(crate) struct ActivePeerTracker {
50    peers: Mutex<HashMap<Endpoint, PeerRecord>>,
51}
52
53impl ActivePeerTracker {
54    /// Records a peer as active, together with the origin it was validated against. Returns whether
55    /// this is the first time it has been seen, which is what earns it an introductory reply.
56    pub(crate) fn upsert(&self, target: &SyncTarget) -> bool {
57        let mut peers = self
58            .peers
59            .lock()
60            .unwrap_or_else(|poisoned| poisoned.into_inner());
61
62        let first_contact = !peers.contains_key(&target.endpoint);
63        if first_contact {
64            info!("Shared-Unlock peer connected {:?}", target.endpoint);
65        }
66        peers.insert(
67            target.endpoint.clone(),
68            PeerRecord {
69                last_seen: Instant::now(),
70                origin: target.origin.clone(),
71            },
72        );
73        first_contact
74    }
75
76    pub(crate) fn targets(&self) -> Vec<SyncTarget> {
77        self.peers
78            .lock()
79            .unwrap_or_else(|poisoned| poisoned.into_inner())
80            .iter()
81            .map(|(endpoint, record)| SyncTarget {
82                endpoint: endpoint.clone(),
83                origin: record.origin.clone(),
84            })
85            .collect()
86    }
87
88    /// Drops peers that have not been heard from within `stale_after`, so this peer stops syncing
89    /// to clients that are no longer running.
90    pub(crate) fn prune_stale(&self, stale_after: Duration) {
91        let mut peers = self
92            .peers
93            .lock()
94            .unwrap_or_else(|poisoned| poisoned.into_inner());
95
96        let now = Instant::now();
97        peers.retain(|endpoint, record| {
98            let alive = now.duration_since(record.last_seen) <= stale_after;
99            if !alive {
100                info!("Shared-Unlock peer {:?} disconnected", endpoint);
101            }
102            alive
103        });
104    }
105}