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