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, 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(&fleet)?),
93 ttl_ms: Arc::new(AtomicU64::new(ttl_ms)),
94 })
95 }
96
97 pub fn set_ttl(
98 &self,
99 ttl: Duration,
100 ) -> io::Result<()> {
101 self.ttl_ms.store(validate_ttl(ttl)?, Ordering::Release);
102 Ok(())
103 }
104
105 pub fn ttl(&self) -> Duration {
106 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
107 }
108
109 pub fn storage(
111 &self,
112 domain: SessionDomain,
113 ) -> Arc<OrbitSessionStorage> {
114 Arc::new(OrbitSessionStorage {
115 primitive: self.primitive.clone(),
116 domain,
117 ttl_ms: self.ttl_ms.clone(),
118 })
119 }
120
121 pub fn reset(&self) -> io::Result<()> {
123 self.primitive.reset()
124 }
125
126 pub fn unlink(&self) -> io::Result<()> {
131 self.primitive.unlink()
132 }
133}
134
135impl fmt::Debug for FleetServerSessions {
136 fn fmt(
137 &self,
138 formatter: &mut fmt::Formatter<'_>,
139 ) -> fmt::Result {
140 formatter
141 .debug_struct("FleetServerSessions")
142 .field("ttl", &self.ttl())
143 .finish_non_exhaustive()
144 }
145}
146
147pub struct OrbitSessionStorage {
148 primitive: Arc<ServerSessionPrimitive>,
149 domain: SessionDomain,
150 ttl_ms: Arc<AtomicU64>,
151}
152
153pub type OrbitServerSessionStorage = OrbitSessionStorage;
157
158impl fmt::Debug for OrbitSessionStorage {
159 fn fmt(
160 &self,
161 formatter: &mut fmt::Formatter<'_>,
162 ) -> fmt::Result {
163 formatter
164 .debug_struct("OrbitSessionStorage")
165 .field("domain", &self.domain)
166 .field(
167 "ttl",
168 &Duration::from_millis(self.ttl_ms.load(Ordering::Acquire)),
169 )
170 .finish_non_exhaustive()
171 }
172}
173
174impl OrbitSessionStorage {
175 fn put_bytes(
176 &self,
177 key: &[u8],
178 value: &[u8],
179 ) -> bool {
180 let ttl = Duration::from_millis(self.ttl_ms.load(Ordering::Acquire));
181 match self.primitive.put(self.domain.as_bytes(), key, value, ttl) {
182 Ok(cached) => {
183 tracing::trace!(
184 target: "orbit_rustls::session_cache",
185 operation = "put",
186 domain = ?self.domain,
187 key_len = key.len(),
188 value_len = value.len(),
189 ?ttl,
190 cached,
191 "rustls session cache write"
192 );
193 cached
194 }
195 Err(error) => {
196 tracing::trace!(
197 target: "orbit_rustls::session_cache",
198 operation = "put",
199 domain = ?self.domain,
200 key_len = key.len(),
201 value_len = value.len(),
202 ?ttl,
203 %error,
204 "rustls session cache write failed"
205 );
206 false
207 }
208 }
209 }
210
211 fn get_bytes(
212 &self,
213 key: &[u8],
214 ) -> Option<Vec<u8>> {
215 match self.primitive.get(self.domain.as_bytes(), key) {
216 Ok(value) => {
217 tracing::trace!(
218 target: "orbit_rustls::session_cache",
219 operation = "get",
220 domain = ?self.domain,
221 key_len = key.len(),
222 value_len = value.as_ref().map_or(0, Vec::len),
223 hit = value.is_some(),
224 "rustls session cache read"
225 );
226 value
227 }
228 Err(error) => {
229 tracing::trace!(
230 target: "orbit_rustls::session_cache",
231 operation = "get",
232 domain = ?self.domain,
233 key_len = key.len(),
234 %error,
235 "rustls session cache read failed"
236 );
237 None
238 }
239 }
240 }
241
242 fn take_bytes(
243 &self,
244 key: &[u8],
245 ) -> Option<Vec<u8>> {
246 match self.primitive.take(self.domain.as_bytes(), key) {
247 Ok(value) => {
248 tracing::trace!(
249 target: "orbit_rustls::session_cache",
250 operation = "take",
251 domain = ?self.domain,
252 key_len = key.len(),
253 value_len = value.as_ref().map_or(0, Vec::len),
254 hit = value.is_some(),
255 "rustls session cache read and consume"
256 );
257 value
258 }
259 Err(error) => {
260 tracing::trace!(
261 target: "orbit_rustls::session_cache",
262 operation = "take",
263 domain = ?self.domain,
264 key_len = key.len(),
265 %error,
266 "rustls session cache read and consume failed"
267 );
268 None
269 }
270 }
271 }
272}
273
274#[cfg(feature = "rustls_0_24")]
280#[derive(Clone)]
281pub struct FleetClientSessions {
282 primitive: Arc<ClientSessionPrimitive>,
283 ttl_ms: Arc<AtomicU64>,
284}
285
286#[cfg(feature = "rustls_0_24")]
287impl FleetClientSessions {
288 pub fn open(fleet: Arc<Fleet>) -> io::Result<Self> {
289 Self::with_ttl(fleet, DEFAULT_SESSION_TTL)
290 }
291
292 pub fn with_ttl(
293 fleet: Arc<Fleet>,
294 ttl: Duration,
295 ) -> io::Result<Self> {
296 let ttl_ms = validate_ttl(ttl)?;
297 Ok(Self {
298 primitive: Arc::new(ClientSessionPrimitive::open(&fleet)?),
299 ttl_ms: Arc::new(AtomicU64::new(ttl_ms)),
300 })
301 }
302
303 pub fn set_ttl(
304 &self,
305 ttl: Duration,
306 ) -> io::Result<()> {
307 self.ttl_ms.store(validate_ttl(ttl)?, Ordering::Release);
308 Ok(())
309 }
310
311 pub fn ttl(&self) -> Duration {
312 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
313 }
314
315 pub fn storage(
316 &self,
317 provider: Arc<rustls_0_24::crypto::CryptoProvider>,
318 ) -> Arc<OrbitClientSessionStorage> {
319 Arc::new(OrbitClientSessionStorage {
320 primitive: self.primitive.clone(),
321 provider,
322 ttl_ms: self.ttl_ms.clone(),
323 })
324 }
325
326 pub fn reset(&self) -> io::Result<()> {
327 self.primitive.reset()
328 }
329
330 pub fn unlink(&self) -> io::Result<()> {
331 self.primitive.unlink()
332 }
333}
334
335#[cfg(feature = "rustls_0_24")]
336impl fmt::Debug for FleetClientSessions {
337 fn fmt(
338 &self,
339 formatter: &mut fmt::Formatter<'_>,
340 ) -> fmt::Result {
341 formatter
342 .debug_struct("FleetClientSessions")
343 .field("ttl", &self.ttl())
344 .finish_non_exhaustive()
345 }
346}
347
348#[cfg(feature = "rustls_0_24")]
349pub struct OrbitClientSessionStorage {
350 primitive: Arc<ClientSessionPrimitive>,
351 provider: Arc<rustls_0_24::crypto::CryptoProvider>,
352 ttl_ms: Arc<AtomicU64>,
353}
354
355#[cfg(feature = "rustls_0_24")]
356impl fmt::Debug for OrbitClientSessionStorage {
357 fn fmt(
358 &self,
359 formatter: &mut fmt::Formatter<'_>,
360 ) -> fmt::Result {
361 formatter
362 .debug_struct("OrbitClientSessionStorage")
363 .field(
364 "ttl",
365 &Duration::from_millis(self.ttl_ms.load(Ordering::Acquire)),
366 )
367 .finish_non_exhaustive()
368 }
369}
370
371#[cfg(feature = "rustls_0_24")]
372impl OrbitClientSessionStorage {
373 fn ttl(&self) -> Duration {
374 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
375 }
376}
377
378fn validate_ttl(ttl: Duration) -> io::Result<u64> {
379 if ttl.is_zero() {
380 return Err(io::Error::new(
381 io::ErrorKind::InvalidInput,
382 "rustls session TTL must be greater than zero",
383 ));
384 }
385 if ttl > MAX_SESSION_TTL {
386 return Err(io::Error::new(
387 io::ErrorKind::InvalidInput,
388 format!(
389 "rustls session TTL {ttl:?} exceeds rustls' stateful lifetime {MAX_SESSION_TTL:?}"
390 ),
391 ));
392 }
393 u64::try_from(ttl.as_millis().max(1)).map_err(|_| {
394 io::Error::new(
395 io::ErrorKind::InvalidInput,
396 "rustls session TTL does not fit milliseconds",
397 )
398 })
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 fn sessions() -> FleetServerSessions {
406 let fleet =
407 Arc::new(Fleet::join("rustls-session-unit", 1).expect("fleet"));
408 FleetServerSessions::open(fleet).expect("sessions")
409 }
410
411 #[test]
412 fn rustls_view_put_get_and_take() {
413 let sessions = sessions();
414 let storage =
415 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
416
417 assert!(storage.put_bytes(b"ticket", b"secret"));
418 assert_eq!(storage.get_bytes(b"ticket"), Some(b"secret".to_vec()));
419 assert_eq!(storage.take_bytes(b"ticket"), Some(b"secret".to_vec()));
420 assert_eq!(storage.take_bytes(b"ticket"), None);
421 }
422
423 #[test]
424 fn domains_are_isolated() {
425 let sessions = sessions();
426 let tcp =
427 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
428 let quic = sessions
429 .storage(SessionDomain::new("quic-public").expect("domain"));
430
431 assert!(tcp.put_bytes(b"same-ticket", b"tcp"));
432 assert_eq!(quic.take_bytes(b"same-ticket"), None);
433 assert_eq!(tcp.take_bytes(b"same-ticket"), Some(b"tcp".to_vec()));
434 }
435
436 #[test]
437 fn existing_views_observe_ttl_updates() {
438 let sessions = sessions();
439 let storage =
440 sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
441 sessions.set_ttl(Duration::from_millis(1)).expect("TTL update");
442
443 assert!(storage.put_bytes(b"short", b"secret"));
444 std::thread::sleep(Duration::from_millis(5));
445 assert_eq!(storage.get_bytes(b"short"), None);
446 }
447
448 #[test]
449 fn ttl_cannot_outlive_rustls_stateful_tickets() {
450 let fleet =
451 Arc::new(Fleet::join("rustls-session-ttl", 1).expect("fleet"));
452
453 assert!(
454 FleetServerSessions::with_ttl(
455 fleet,
456 MAX_SESSION_TTL + Duration::from_millis(1)
457 )
458 .is_err()
459 );
460 }
461}