1use std::sync::Arc;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::time::Duration;
4use std::{fmt, io};
5
6use orbit_core::Fleet;
7
8#[cfg(feature = "rustls_0_24")]
9use self::store::ClientSessionPrimitive;
10use self::store::{DOMAIN_MAX, Exclusion, ServerSessionPrimitive};
11
12#[cfg(feature = "rustls_0_24")]
13mod client_v24;
14mod store;
15#[cfg(feature = "rustls_0_23")]
16mod v23;
17#[cfg(feature = "rustls_0_24")]
18mod v24;
19
20pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
24pub const MAX_SESSION_TTL: Duration = DEFAULT_SESSION_TTL;
25pub const SERVER_SESSION_STATE_KIND: u8 = store::SERVER_SESSION_STATE_KIND;
26#[cfg(feature = "rustls_0_24")]
27pub const CLIENT_SESSION_STATE_KIND: u8 = store::CLIENT_SESSION_STATE_KIND;
28
29#[derive(Clone, PartialEq, Eq, Hash)]
34pub struct SessionDomain(Arc<[u8]>);
35
36impl SessionDomain {
37 pub fn new(value: impl AsRef<[u8]>) -> io::Result<Self> {
38 let value = value.as_ref();
39 if value.is_empty() {
40 return Err(io::Error::new(
41 io::ErrorKind::InvalidInput,
42 "rustls session domain must not be empty",
43 ));
44 }
45 if value.len() > DOMAIN_MAX {
46 return Err(io::Error::new(
47 io::ErrorKind::InvalidInput,
48 format!(
49 "rustls session domain is too long: {} bytes (maximum {DOMAIN_MAX})",
50 value.len()
51 ),
52 ));
53 }
54 Ok(Self(Arc::from(value)))
55 }
56
57 fn as_bytes(&self) -> &[u8] {
58 &self.0
59 }
60}
61
62impl fmt::Debug for SessionDomain {
63 fn fmt(
64 &self,
65 formatter: &mut fmt::Formatter<'_>,
66 ) -> fmt::Result {
67 formatter
68 .debug_tuple("SessionDomain")
69 .field(&String::from_utf8_lossy(&self.0))
70 .finish()
71 }
72}
73
74#[derive(Clone)]
76pub struct FleetServerSessions {
77 primitive: Arc<ServerSessionPrimitive>,
78 ttl_ms: Arc<AtomicU64>,
79}
80
81impl FleetServerSessions {
82 pub fn open(fleet: Arc<Fleet>) -> io::Result<Self> {
83 Self::with_ttl(fleet, DEFAULT_SESSION_TTL)
84 }
85
86 pub fn with_ttl(
87 fleet: Arc<Fleet>,
88 ttl: Duration,
89 ) -> io::Result<Self> {
90 let ttl_ms = validate_ttl(ttl)?;
91 Ok(Self {
92 primitive: Arc::new(ServerSessionPrimitive::open(
93 &fleet,
94 Exclusion::Set,
95 )?),
96 ttl_ms: Arc::new(AtomicU64::new(ttl_ms)),
97 })
98 }
99
100 pub fn set_ttl(
101 &self,
102 ttl: Duration,
103 ) -> io::Result<()> {
104 self.ttl_ms.store(validate_ttl(ttl)?, Ordering::Release);
105 Ok(())
106 }
107
108 pub fn ttl(&self) -> Duration {
109 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
110 }
111
112 pub fn storage(
114 &self,
115 domain: SessionDomain,
116 ) -> Arc<OrbitSessionStorage> {
117 Arc::new(OrbitSessionStorage {
118 primitive: self.primitive.clone(),
119 domain,
120 ttl_ms: self.ttl_ms.clone(),
121 })
122 }
123
124 pub fn reset(&self) -> io::Result<()> {
129 self.primitive.reset()
130 }
131
132 pub fn unlink(&self) -> io::Result<()> {
137 self.primitive.unlink()
138 }
139}
140
141impl fmt::Debug for FleetServerSessions {
142 fn fmt(
143 &self,
144 formatter: &mut fmt::Formatter<'_>,
145 ) -> fmt::Result {
146 formatter
147 .debug_struct("FleetServerSessions")
148 .field("ttl", &self.ttl())
149 .finish_non_exhaustive()
150 }
151}
152
153pub struct OrbitSessionStorage {
154 primitive: Arc<ServerSessionPrimitive>,
155 domain: SessionDomain,
156 ttl_ms: Arc<AtomicU64>,
157}
158
159pub type OrbitServerSessionStorage = OrbitSessionStorage;
163
164impl fmt::Debug for OrbitSessionStorage {
165 fn fmt(
166 &self,
167 formatter: &mut fmt::Formatter<'_>,
168 ) -> fmt::Result {
169 formatter
170 .debug_struct("OrbitSessionStorage")
171 .field("domain", &self.domain)
172 .field(
173 "ttl",
174 &Duration::from_millis(self.ttl_ms.load(Ordering::Acquire)),
175 )
176 .finish_non_exhaustive()
177 }
178}
179
180impl OrbitSessionStorage {
181 fn put_bytes(
182 &self,
183 key: &[u8],
184 value: &[u8],
185 ) -> bool {
186 let ttl = Duration::from_millis(self.ttl_ms.load(Ordering::Acquire));
187 match self.primitive.put(self.domain.as_bytes(), key, value, ttl) {
188 Ok(cached) => {
189 tracing::trace!(
190 target: "orbit_rustls::session_cache",
191 operation = "put",
192 domain = ?self.domain,
193 key_len = key.len(),
194 value_len = value.len(),
195 ?ttl,
196 cached,
197 "rustls session cache write"
198 );
199 cached
200 }
201 Err(error) => {
202 tracing::trace!(
203 target: "orbit_rustls::session_cache",
204 operation = "put",
205 domain = ?self.domain,
206 key_len = key.len(),
207 value_len = value.len(),
208 ?ttl,
209 %error,
210 "rustls session cache write failed"
211 );
212 false
213 }
214 }
215 }
216
217 fn get_bytes(
218 &self,
219 key: &[u8],
220 ) -> Option<Vec<u8>> {
221 match self.primitive.get(self.domain.as_bytes(), key) {
222 Ok(value) => {
223 tracing::trace!(
224 target: "orbit_rustls::session_cache",
225 operation = "get",
226 domain = ?self.domain,
227 key_len = key.len(),
228 value_len = value.as_ref().map_or(0, Vec::len),
229 hit = value.is_some(),
230 "rustls session cache read"
231 );
232 value
233 }
234 Err(error) => {
235 tracing::trace!(
236 target: "orbit_rustls::session_cache",
237 operation = "get",
238 domain = ?self.domain,
239 key_len = key.len(),
240 %error,
241 "rustls session cache read failed"
242 );
243 None
244 }
245 }
246 }
247
248 fn take_bytes(
249 &self,
250 key: &[u8],
251 ) -> Option<Vec<u8>> {
252 match self.primitive.take(self.domain.as_bytes(), key) {
253 Ok(value) => {
254 tracing::trace!(
255 target: "orbit_rustls::session_cache",
256 operation = "take",
257 domain = ?self.domain,
258 key_len = key.len(),
259 value_len = value.as_ref().map_or(0, Vec::len),
260 hit = value.is_some(),
261 "rustls session cache read and consume"
262 );
263 value
264 }
265 Err(error) => {
266 tracing::trace!(
267 target: "orbit_rustls::session_cache",
268 operation = "take",
269 domain = ?self.domain,
270 key_len = key.len(),
271 %error,
272 "rustls session cache read and consume failed"
273 );
274 None
275 }
276 }
277 }
278}
279
280#[cfg(feature = "rustls_0_24")]
286#[derive(Clone)]
287pub struct FleetClientSessions {
288 primitive: Arc<ClientSessionPrimitive>,
289 ttl_ms: Arc<AtomicU64>,
290}
291
292#[cfg(feature = "rustls_0_24")]
293impl FleetClientSessions {
294 pub fn open(fleet: Arc<Fleet>) -> io::Result<Self> {
295 Self::with_ttl(fleet, DEFAULT_SESSION_TTL)
296 }
297
298 pub fn with_ttl(
299 fleet: Arc<Fleet>,
300 ttl: Duration,
301 ) -> io::Result<Self> {
302 let ttl_ms = validate_ttl(ttl)?;
303 Ok(Self {
304 primitive: Arc::new(ClientSessionPrimitive::open(
305 &fleet,
306 Exclusion::Table,
307 )?),
308 ttl_ms: Arc::new(AtomicU64::new(ttl_ms)),
309 })
310 }
311
312 pub fn set_ttl(
313 &self,
314 ttl: Duration,
315 ) -> io::Result<()> {
316 self.ttl_ms.store(validate_ttl(ttl)?, Ordering::Release);
317 Ok(())
318 }
319
320 pub fn ttl(&self) -> Duration {
321 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
322 }
323
324 pub fn storage(
325 &self,
326 provider: Arc<rustls_0_24::crypto::CryptoProvider>,
327 ) -> Arc<OrbitClientSessionStorage> {
328 Arc::new(OrbitClientSessionStorage {
329 primitive: self.primitive.clone(),
330 provider,
331 ttl_ms: self.ttl_ms.clone(),
332 })
333 }
334
335 pub fn reset(&self) -> io::Result<()> {
336 self.primitive.reset()
337 }
338
339 pub fn unlink(&self) -> io::Result<()> {
340 self.primitive.unlink()
341 }
342}
343
344#[cfg(feature = "rustls_0_24")]
345impl fmt::Debug for FleetClientSessions {
346 fn fmt(
347 &self,
348 formatter: &mut fmt::Formatter<'_>,
349 ) -> fmt::Result {
350 formatter
351 .debug_struct("FleetClientSessions")
352 .field("ttl", &self.ttl())
353 .finish_non_exhaustive()
354 }
355}
356
357#[cfg(feature = "rustls_0_24")]
358pub struct OrbitClientSessionStorage {
359 primitive: Arc<ClientSessionPrimitive>,
360 provider: Arc<rustls_0_24::crypto::CryptoProvider>,
361 ttl_ms: Arc<AtomicU64>,
362}
363
364#[cfg(feature = "rustls_0_24")]
365impl fmt::Debug for OrbitClientSessionStorage {
366 fn fmt(
367 &self,
368 formatter: &mut fmt::Formatter<'_>,
369 ) -> fmt::Result {
370 formatter
371 .debug_struct("OrbitClientSessionStorage")
372 .field(
373 "ttl",
374 &Duration::from_millis(self.ttl_ms.load(Ordering::Acquire)),
375 )
376 .finish_non_exhaustive()
377 }
378}
379
380#[cfg(feature = "rustls_0_24")]
381impl OrbitClientSessionStorage {
382 fn ttl(&self) -> Duration {
383 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
384 }
385}
386
387fn validate_ttl(ttl: Duration) -> io::Result<u64> {
388 if ttl.is_zero() {
389 return Err(io::Error::new(
390 io::ErrorKind::InvalidInput,
391 "rustls session TTL must be greater than zero",
392 ));
393 }
394 if ttl > MAX_SESSION_TTL {
395 return Err(io::Error::new(
396 io::ErrorKind::InvalidInput,
397 format!(
398 "rustls session TTL {ttl:?} exceeds rustls' stateful lifetime {MAX_SESSION_TTL:?}"
399 ),
400 ));
401 }
402 u64::try_from(ttl.as_millis().max(1)).map_err(|_| {
403 io::Error::new(
404 io::ErrorKind::InvalidInput,
405 "rustls session TTL does not fit milliseconds",
406 )
407 })
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 fn sessions() -> FleetServerSessions {
415 let fleet =
416 Arc::new(Fleet::join("rustls-session-unit", 1).expect("fleet"));
417 FleetServerSessions::open(fleet).expect("sessions")
418 }
419
420 #[test]
421 fn rustls_view_put_get_and_take() {
422 let sessions = sessions();
423 let storage =
424 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
425
426 assert!(storage.put_bytes(b"ticket", b"secret"));
427 assert_eq!(storage.get_bytes(b"ticket"), Some(b"secret".to_vec()));
428 assert_eq!(storage.take_bytes(b"ticket"), Some(b"secret".to_vec()));
429 assert_eq!(storage.take_bytes(b"ticket"), None);
430 }
431
432 #[test]
433 fn domains_are_isolated() {
434 let sessions = sessions();
435 let tcp =
436 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
437 let quic = sessions
438 .storage(SessionDomain::new("quic-public").expect("domain"));
439
440 assert!(tcp.put_bytes(b"same-ticket", b"tcp"));
441 assert_eq!(quic.take_bytes(b"same-ticket"), None);
442 assert_eq!(tcp.take_bytes(b"same-ticket"), Some(b"tcp".to_vec()));
443 }
444
445 #[test]
446 fn existing_views_observe_ttl_updates() {
447 let sessions = sessions();
448 let storage =
449 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
450 sessions.set_ttl(Duration::from_millis(1)).expect("TTL update");
451
452 assert!(storage.put_bytes(b"short", b"secret"));
453 std::thread::sleep(Duration::from_millis(5));
454 assert_eq!(storage.get_bytes(b"short"), None);
455 }
456
457 #[test]
458 fn ttl_cannot_outlive_rustls_stateful_tickets() {
459 let fleet =
460 Arc::new(Fleet::join("rustls-session-ttl", 1).expect("fleet"));
461
462 assert!(
463 FleetServerSessions::with_ttl(
464 fleet,
465 MAX_SESSION_TTL + Duration::from_millis(1)
466 )
467 .is_err()
468 );
469 }
470}