liminal_server/
link_federated.rs1use serde_json::Value;
4use std::collections::BTreeMap;
5use std::num::NonZeroUsize;
6use std::sync::{Mutex, MutexGuard};
7
8#[path = "link_federated/projection.rs"]
9mod projection;
10
11#[derive(Clone, Copy, Debug)]
13pub struct ProjectionLimits {
14 pub bytes: NonZeroUsize,
16 pub stations: NonZeroUsize,
18 pub peers: NonZeroUsize,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
24pub enum DiscoveryError {
25 #[error("far_topology_bound")]
27 Bound,
28 #[error("far_topology_invalid")]
30 Invalid,
31 #[error("far_topology_stale")]
33 Stale,
34 #[error("far_topology_unavailable")]
36 Unavailable,
37 #[error("far_topology_generation_exhausted")]
39 Exhausted,
40}
41
42#[derive(Default)]
43struct State {
44 generation: u64,
45 links: BTreeMap<String, FarLink>,
46}
47struct FarLink {
48 generation: u64,
49 peer: String,
50 prefixes: Vec<String>,
51 revision: Option<u64>,
52 document: Option<Value>,
53}
54
55pub struct FarTopology {
57 limits: ProjectionLimits,
58 state: Mutex<State>,
59}
60
61impl std::fmt::Debug for FarTopology {
62 fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 out.debug_struct("FarTopology")
64 .field("limits", &self.limits)
65 .finish_non_exhaustive()
66 }
67}
68
69impl FarTopology {
70 #[must_use]
72 pub fn new(limits: ProjectionLimits) -> Self {
73 Self {
74 limits,
75 state: Mutex::default(),
76 }
77 }
78
79 fn lock(&self) -> Result<MutexGuard<'_, State>, DiscoveryError> {
80 self.state.lock().map_err(|_| DiscoveryError::Unavailable)
81 }
82
83 pub fn open(
88 &self,
89 link: &str,
90 peer: &str,
91 prefixes: Vec<String>,
92 ) -> Result<u64, DiscoveryError> {
93 if link.is_empty()
94 || peer.is_empty()
95 || prefixes.is_empty()
96 || link
97 .chars()
98 .chain(peer.chars())
99 .any(|character| character.is_control() || character.is_whitespace())
100 {
101 return Err(DiscoveryError::Invalid);
102 }
103 for prefix in &prefixes {
104 crate::auth_lys::SubjectBinding::new("wire".into(), prefix.clone(), prefix.clone())
105 .map_err(|_| DiscoveryError::Invalid)?;
106 if prefix == "liminal"
107 || prefix.starts_with("liminal.")
108 || prefix == "console"
109 || prefix.starts_with("console.")
110 {
111 return Err(DiscoveryError::Invalid);
112 }
113 }
114 let mut state = self.lock()?;
115 if !state.links.contains_key(link) && state.links.len() >= self.limits.peers.get() {
116 return Err(DiscoveryError::Bound);
117 }
118 state.generation = state
119 .generation
120 .checked_add(1)
121 .ok_or(DiscoveryError::Exhausted)?;
122 let generation = state.generation;
123 state.links.insert(
124 link.into(),
125 FarLink {
126 generation,
127 peer: peer.into(),
128 prefixes,
129 revision: None,
130 document: None,
131 },
132 );
133 drop(state);
134 Ok(generation)
135 }
136
137 pub fn update(
142 &self,
143 link: &str,
144 generation: u64,
145 bytes: &[u8],
146 ) -> Result<bool, DiscoveryError> {
147 if bytes.len() > self.limits.bytes.get() {
148 return Err(DiscoveryError::Bound);
149 }
150 let document: Value = serde_json::from_slice(bytes).map_err(|_| DiscoveryError::Invalid)?;
151 let revision = projection::validate(&document, self.limits.stations.get())?;
152 let mut state = self.lock()?;
153 let far = state
154 .links
155 .get_mut(link)
156 .filter(|far| far.generation == generation)
157 .ok_or(DiscoveryError::Stale)?;
158 if far.revision.is_some_and(|previous| previous > revision) {
159 return Err(DiscoveryError::Stale);
160 }
161 if far.revision == Some(revision) {
162 return if far.document.as_ref() == Some(&document) {
163 Ok(false)
164 } else {
165 Err(DiscoveryError::Stale)
166 };
167 }
168 far.revision = Some(revision);
169 far.document = Some(document);
170 drop(state);
171 Ok(true)
172 }
173
174 pub fn close(&self, link: &str, generation: u64) -> Result<bool, DiscoveryError> {
179 let mut state = self.lock()?;
180 if state
181 .links
182 .get(link)
183 .is_some_and(|far| far.generation == generation)
184 {
185 state.links.remove(link);
186 drop(state);
187 Ok(true)
188 } else {
189 Ok(false)
190 }
191 }
192
193 pub fn pending_peers(&self) -> Result<usize, DiscoveryError> {
198 Ok(self.lock()?.links.len())
199 }
200
201 pub fn describe(&self, subject: &str) -> Result<Option<Value>, DiscoveryError> {
206 let state = self.lock()?;
207 for (link, far) in &state.links {
208 for station in projection::stations(link, far)? {
209 if station["subjects"].as_array().is_some_and(|subjects| {
210 subjects.iter().any(|item| item.as_str() == Some(subject))
211 }) {
212 return Ok(Some(station));
213 }
214 }
215 }
216 drop(state);
217 Ok(None)
218 }
219
220 pub fn project(&self, bytes: &[u8]) -> Result<Vec<u8>, DiscoveryError> {
226 projection::project(self, bytes)
227 }
228}
229
230fn reaches(prefix: &str, subject: &str) -> bool {
231 subject == prefix
232 || subject
233 .strip_prefix(prefix)
234 .is_some_and(|suffix| suffix.starts_with('.'))
235}