1use std::sync::Arc;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::time::Duration;
4use std::{fmt, io};
5
6use orbit_core::Fleet;
7
8use self::store::{DOMAIN_MAX, SessionPrimitive};
9
10mod store;
11#[cfg(feature = "rustls_0_23")]
12mod v23;
13#[cfg(feature = "rustls_0_24")]
14mod v24;
15
16pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
20pub const MAX_SESSION_TTL: Duration = DEFAULT_SESSION_TTL;
21
22#[derive(Clone, PartialEq, Eq, Hash)]
27pub struct SessionDomain(Arc<[u8]>);
28
29impl SessionDomain {
30 pub fn new(value: impl AsRef<[u8]>) -> io::Result<Self> {
31 let value = value.as_ref();
32 if value.is_empty() {
33 return Err(io::Error::new(
34 io::ErrorKind::InvalidInput,
35 "rustls session domain must not be empty"
36 ));
37 }
38 if value.len() > DOMAIN_MAX {
39 return Err(io::Error::new(
40 io::ErrorKind::InvalidInput,
41 format!(
42 "rustls session domain is too long: {} bytes (maximum {DOMAIN_MAX})",
43 value.len()
44 )
45 ));
46 }
47 Ok(Self(Arc::from(value)))
48 }
49
50 fn as_bytes(&self) -> &[u8] {
51 &self.0
52 }
53}
54
55impl fmt::Debug for SessionDomain {
56 fn fmt(
57 &self,
58 formatter: &mut fmt::Formatter<'_>
59 ) -> fmt::Result {
60 formatter.debug_tuple("SessionDomain").field(&String::from_utf8_lossy(&self.0)).finish()
61 }
62}
63
64#[derive(Clone)]
66pub struct FleetServerSessions {
67 primitive: Arc<SessionPrimitive>,
68 ttl_ms: Arc<AtomicU64>
69}
70
71impl FleetServerSessions {
72 pub fn open(fleet: Arc<Fleet>) -> io::Result<Self> {
73 Self::with_ttl(fleet, DEFAULT_SESSION_TTL)
74 }
75
76 pub fn with_ttl(
77 fleet: Arc<Fleet>,
78 ttl: Duration
79 ) -> io::Result<Self> {
80 let ttl_ms = validate_ttl(ttl)?;
81 Ok(Self {
82 primitive: Arc::new(SessionPrimitive::open(&fleet)?),
83 ttl_ms: Arc::new(AtomicU64::new(ttl_ms))
84 })
85 }
86
87 pub fn set_ttl(
88 &self,
89 ttl: Duration
90 ) -> io::Result<()> {
91 self.ttl_ms.store(validate_ttl(ttl)?, Ordering::Release);
92 Ok(())
93 }
94
95 pub fn ttl(&self) -> Duration {
96 Duration::from_millis(self.ttl_ms.load(Ordering::Acquire))
97 }
98
99 pub fn storage(
101 &self,
102 domain: SessionDomain
103 ) -> Arc<OrbitSessionStorage> {
104 Arc::new(OrbitSessionStorage {
105 primitive: self.primitive.clone(),
106 domain,
107 ttl_ms: self.ttl_ms.clone()
108 })
109 }
110
111 pub fn reset(&self) -> io::Result<()> {
113 self.primitive.reset()
114 }
115
116 pub fn unlink(&self) -> io::Result<()> {
121 self.primitive.unlink()
122 }
123}
124
125impl fmt::Debug for FleetServerSessions {
126 fn fmt(
127 &self,
128 formatter: &mut fmt::Formatter<'_>
129 ) -> fmt::Result {
130 formatter
131 .debug_struct("FleetServerSessions")
132 .field("ttl", &self.ttl())
133 .finish_non_exhaustive()
134 }
135}
136
137pub struct OrbitSessionStorage {
138 primitive: Arc<SessionPrimitive>,
139 domain: SessionDomain,
140 ttl_ms: Arc<AtomicU64>
141}
142
143impl fmt::Debug for OrbitSessionStorage {
144 fn fmt(
145 &self,
146 formatter: &mut fmt::Formatter<'_>
147 ) -> fmt::Result {
148 formatter
149 .debug_struct("OrbitSessionStorage")
150 .field("domain", &self.domain)
151 .field("ttl", &Duration::from_millis(self.ttl_ms.load(Ordering::Acquire)))
152 .finish_non_exhaustive()
153 }
154}
155
156impl OrbitSessionStorage {
157 fn put_bytes(
158 &self,
159 key: &[u8],
160 value: &[u8]
161 ) -> bool {
162 let ttl = Duration::from_millis(self.ttl_ms.load(Ordering::Acquire));
163 match self.primitive.put(self.domain.as_bytes(), key, value, ttl) {
164 Ok(cached) => {
165 tracing::trace!(
166 target: "orbit_rustls::session_cache",
167 operation = "put",
168 domain = ?self.domain,
169 key_len = key.len(),
170 value_len = value.len(),
171 ?ttl,
172 cached,
173 "rustls session cache write"
174 );
175 cached
176 }
177 Err(error) => {
178 tracing::trace!(
179 target: "orbit_rustls::session_cache",
180 operation = "put",
181 domain = ?self.domain,
182 key_len = key.len(),
183 value_len = value.len(),
184 ?ttl,
185 %error,
186 "rustls session cache write failed"
187 );
188 false
189 }
190 }
191 }
192
193 fn get_bytes(
194 &self,
195 key: &[u8]
196 ) -> Option<Vec<u8>> {
197 match self.primitive.get(self.domain.as_bytes(), key) {
198 Ok(value) => {
199 tracing::trace!(
200 target: "orbit_rustls::session_cache",
201 operation = "get",
202 domain = ?self.domain,
203 key_len = key.len(),
204 value_len = value.as_ref().map_or(0, Vec::len),
205 hit = value.is_some(),
206 "rustls session cache read"
207 );
208 value
209 }
210 Err(error) => {
211 tracing::trace!(
212 target: "orbit_rustls::session_cache",
213 operation = "get",
214 domain = ?self.domain,
215 key_len = key.len(),
216 %error,
217 "rustls session cache read failed"
218 );
219 None
220 }
221 }
222 }
223
224 fn take_bytes(
225 &self,
226 key: &[u8]
227 ) -> Option<Vec<u8>> {
228 match self.primitive.take(self.domain.as_bytes(), key) {
229 Ok(value) => {
230 tracing::trace!(
231 target: "orbit_rustls::session_cache",
232 operation = "take",
233 domain = ?self.domain,
234 key_len = key.len(),
235 value_len = value.as_ref().map_or(0, Vec::len),
236 hit = value.is_some(),
237 "rustls session cache read and consume"
238 );
239 value
240 }
241 Err(error) => {
242 tracing::trace!(
243 target: "orbit_rustls::session_cache",
244 operation = "take",
245 domain = ?self.domain,
246 key_len = key.len(),
247 %error,
248 "rustls session cache read and consume failed"
249 );
250 None
251 }
252 }
253 }
254}
255
256fn validate_ttl(ttl: Duration) -> io::Result<u64> {
257 if ttl.is_zero() {
258 return Err(io::Error::new(
259 io::ErrorKind::InvalidInput,
260 "rustls session TTL must be greater than zero"
261 ));
262 }
263 if ttl > MAX_SESSION_TTL {
264 return Err(io::Error::new(
265 io::ErrorKind::InvalidInput,
266 format!(
267 "rustls session TTL {ttl:?} exceeds rustls' stateful lifetime {MAX_SESSION_TTL:?}"
268 )
269 ));
270 }
271 u64::try_from(ttl.as_millis().max(1)).map_err(|_| {
272 io::Error::new(io::ErrorKind::InvalidInput, "rustls session TTL does not fit milliseconds")
273 })
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 fn sessions() -> FleetServerSessions {
281 let fleet = Arc::new(Fleet::join("rustls-session-unit", 1).expect("fleet"));
282 FleetServerSessions::open(fleet).expect("sessions")
283 }
284
285 #[test]
286 fn rustls_view_put_get_and_take() {
287 let sessions = sessions();
288 let storage = sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
289
290 assert!(storage.put_bytes(b"ticket", b"secret"));
291 assert_eq!(storage.get_bytes(b"ticket"), Some(b"secret".to_vec()));
292 assert_eq!(storage.take_bytes(b"ticket"), Some(b"secret".to_vec()));
293 assert_eq!(storage.take_bytes(b"ticket"), None);
294 }
295
296 #[test]
297 fn domains_are_isolated() {
298 let sessions = sessions();
299 let tcp = sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
300 let quic = sessions.storage(SessionDomain::new("quic-public").expect("domain"));
301
302 assert!(tcp.put_bytes(b"same-ticket", b"tcp"));
303 assert_eq!(quic.take_bytes(b"same-ticket"), None);
304 assert_eq!(tcp.take_bytes(b"same-ticket"), Some(b"tcp".to_vec()));
305 }
306
307 #[test]
308 fn existing_views_observe_ttl_updates() {
309 let sessions = sessions();
310 let storage = sessions.storage(SessionDomain::new("tcp-public").expect("domain"));
311 sessions.set_ttl(Duration::from_millis(1)).expect("TTL update");
312
313 assert!(storage.put_bytes(b"short", b"secret"));
314 std::thread::sleep(Duration::from_millis(5));
315 assert_eq!(storage.get_bytes(b"short"), None);
316 }
317
318 #[test]
319 fn ttl_cannot_outlive_rustls_stateful_tickets() {
320 let fleet = Arc::new(Fleet::join("rustls-session-ttl", 1).expect("fleet"));
321
322 assert!(
323 FleetServerSessions::with_ttl(fleet, MAX_SESSION_TTL + Duration::from_millis(1))
324 .is_err()
325 );
326 }
327}