Skip to main content

ri/auth/
session.rs

1//! Copyright © 2025 Wenze Wei. All Rights Reserved.
2//! 
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//! 
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//! 
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//! 
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! Session management implementation for Ri.
19//! 
20//! This module provides session management functionality, including:
21//! - Session creation and validation
22//! - Session expiration tracking
23//! - User session management
24//! - Session data storage
25//! - Expired session cleanup
26//! 
27//! # Design Principles
28//! - **Security**: Session IDs are generated using UUID v4 for uniqueness
29//! - **Performance**: Efficient session lookup with hash maps
30//! - **Flexibility**: Supports custom session timeout and data storage
31//! - **Scalability**: Limits sessions per user to prevent resource exhaustion
32//! - **Convenience**: Automatically cleans up expired sessions
33//! - **Thread Safety**: Uses RwLock for concurrent access to session storage
34//! 
35//! # Usage Examples
36//! ```rust
37//! // Create a session manager with 30-minute timeout
38//! let session_manager = RiSessionManager::new(1800);
39//! 
40//! // Create a new session for a user
41//! let session_id = session_manager.create_session(
42//!     "user123".to_string(),
43//!     Some("192.168.1.1".to_string()),
44//!     Some("Mozilla/5.0".to_string())
45//! ).await?;
46//! 
47//! // Get session data
48//! let session = session_manager.get_session(&session_id).await?;
49//! 
50//! // Update session data
51//! let mut data = FxHashMap::default();
52//! data.insert("theme".to_string(), "dark".to_string());
53//! session_manager.update_session(&session_id, data).await?;
54//! 
55//! // Destroy a session
56//! session_manager.destroy_session(&session_id).await?;
57//! 
58//! // Cleanup expired sessions
59//! let cleaned_count = session_manager.cleanup_expired().await?;
60//! ```
61
62#![allow(non_snake_case)]
63
64use serde::{Deserialize, Serialize};
65use std::collections::HashMap as FxHashMap;
66use std::time::{SystemTime, UNIX_EPOCH};
67use uuid::Uuid;
68use crate::core::concurrent::RiShardedLock;
69
70#[cfg(feature = "pyo3")]
71use pyo3::PyResult;
72
73/// Maximum number of data entries in a session
74const MAX_SESSION_DATA_ENTRIES: usize = 100;
75
76/// Maximum length of a session data key
77const MAX_SESSION_DATA_KEY_LENGTH: usize = 256;
78
79/// Maximum length of a session data value
80const MAX_SESSION_DATA_VALUE_LENGTH: usize = 4096;
81
82/// Maximum length of user agent string
83#[allow(dead_code)]
84const MAX_USER_AGENT_LENGTH: usize = 1024;
85
86/// Session structure for tracking user sessions.
87///
88/// This struct represents a user session with metadata, expiration tracking,
89/// and custom data storage. Sessions are uniquely identified by UUIDs.
90#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct RiSession {
93    /// Unique session identifier generated using UUID v4
94    pub id: String,
95    /// User ID associated with this session
96    pub user_id: String,
97    /// Session creation time as Unix timestamp
98    pub created_at: u64,
99    /// Last access time as Unix timestamp (updated on each access)
100    pub last_accessed: u64,
101    /// Session expiration time as Unix timestamp
102    pub expires_at: u64,
103    /// Custom key-value data associated with the session
104    pub data: FxHashMap<String, String>,
105    /// Client IP address from which the session was created
106    pub ip_address: Option<String>,
107    /// Client user agent string from which the session was created
108    pub user_agent: Option<String>,
109}
110
111#[cfg(feature = "pyo3")]
112#[pyo3::prelude::pymethods]
113impl RiSession {
114    #[new]
115    fn py_new(
116        id: Option<String>,
117        user_id: String,
118        created_at: Option<u64>,
119        last_accessed: Option<u64>,
120        expires_at: Option<u64>,
121        data: Option<FxHashMap<String, String>>,
122        ip_address: Option<String>,
123        user_agent: Option<String>,
124    ) -> Self {
125        let now = SystemTime::now()
126            .duration_since(UNIX_EPOCH)
127            .map_or(0, |d| d.as_secs());
128        
129        Self {
130            id: id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
131            user_id,
132            created_at: created_at.unwrap_or(now),
133            last_accessed: last_accessed.unwrap_or(now),
134            expires_at: expires_at.unwrap_or(now + 28800),
135            data: data.unwrap_or_default(),
136            ip_address,
137            user_agent,
138        }
139    }
140}
141
142impl RiSession {
143    /// Creates a new session for a user.
144    /// 
145    /// # Parameters
146    /// - `user_id`: ID of the user to create the session for
147    /// - `timeout_secs`: Session timeout in seconds
148    /// - `ip_address`: Optional client IP address
149    /// - `user_agent`: Optional client user agent
150    /// 
151    /// # Returns
152    /// A new instance of `RiSession`
153    pub fn new(user_id: String, timeout_secs: u64, ip_address: Option<String>, user_agent: Option<String>) -> Self {
154        let now = SystemTime::now()
155            .duration_since(UNIX_EPOCH)
156            .map_or(0, |d| d.as_secs());
157
158        Self {
159            id: Uuid::new_v4().to_string(),
160            user_id,
161            created_at: now,
162            last_accessed: now,
163            expires_at: now + timeout_secs,
164            data: FxHashMap::default(),
165            ip_address,
166            user_agent,
167        }
168    }
169
170    /// Checks if the session has expired.
171    /// 
172    /// # Returns
173    /// `true` if the session has expired, otherwise `false`
174    pub fn is_expired(&self) -> bool {
175        let now = SystemTime::now()
176            .duration_since(UNIX_EPOCH)
177            .map_or(0, |d| d.as_secs());
178        now > self.expires_at
179    }
180
181    /// Updates the last accessed time of the session.
182    /// 
183    /// This method is called when a session is accessed to update its
184    /// last accessed timestamp, which can be used for session activity tracking.
185    pub fn touch(&mut self) {
186        let now = SystemTime::now()
187            .duration_since(UNIX_EPOCH)
188            .map_or(0, |d| d.as_secs());
189        self.last_accessed = now;
190    }
191
192    /// Extends the session expiration time.
193    /// 
194    /// # Parameters
195    /// - `timeout_secs`: New timeout in seconds from the current time
196    pub fn extend(&mut self, timeout_secs: u64) {
197        let now = SystemTime::now()
198            .duration_since(UNIX_EPOCH)
199            .map_or(0, |d| d.as_secs());
200        self.expires_at = now + timeout_secs;
201    }
202
203    /// Gets a value from the session data.
204    /// 
205    /// # Parameters
206    /// - `key`: Key to look up in the session data
207    /// 
208    /// # Returns
209    /// `Some(&String)` if the key exists, otherwise `None`
210    pub fn get_data(&self, key: &str) -> Option<&String> {
211        self.data.get(key)
212    }
213
214    /// Sets a value in the session data.
215    /// 
216    /// # Security
217    /// 
218    /// This method validates:
219    /// - Maximum number of data entries (100)
220    /// - Maximum key length (256 characters)
221    /// - Maximum value length (4096 characters)
222    /// 
223    /// # Parameters
224    /// - `key`: Key to set in the session data
225    /// - `value`: Value to associate with the key
226    /// 
227    /// # Returns
228    /// `true` if the value was set, `false` if limits exceeded
229    pub fn set_data(&mut self, key: String, value: String) -> bool {
230        // Security: Check key length
231        if key.len() > MAX_SESSION_DATA_KEY_LENGTH {
232            log::warn!(
233                "[Ri.Session] Data key too long: {} chars (max {})",
234                key.len(), MAX_SESSION_DATA_KEY_LENGTH
235            );
236            return false;
237        }
238        
239        // Security: Check value length
240        let safe_value = if value.len() > MAX_SESSION_DATA_VALUE_LENGTH {
241            log::warn!(
242                "[Ri.Session] Data value truncated: {} chars (max {})",
243                value.len(), MAX_SESSION_DATA_VALUE_LENGTH
244            );
245            value[..MAX_SESSION_DATA_VALUE_LENGTH].to_string()
246        } else {
247            value
248        };
249        
250        // Security: Check maximum entries (only for new keys)
251        if !self.data.contains_key(&key) && self.data.len() >= MAX_SESSION_DATA_ENTRIES {
252            log::warn!(
253                "[Ri.Session] Maximum data entries reached: {} (max {})",
254                self.data.len(), MAX_SESSION_DATA_ENTRIES
255            );
256            return false;
257        }
258        
259        self.data.insert(key, safe_value);
260        true
261    }
262
263    /// Removes a value from the session data.
264    /// 
265    /// # Parameters
266    /// - `key`: Key to remove from the session data
267    /// 
268    /// # Returns
269    /// `Some(String)` if the key existed and was removed, otherwise `None`
270    pub fn remove_data(&mut self, key: &str) -> Option<String> {
271        self.data.remove(key)
272    }
273}
274
275/// Session manager for handling user sessions.
276///
277/// This struct manages session creation, validation, and cleanup. It limits
278/// the number of sessions per user and automatically cleans up expired sessions.
279#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
280pub struct RiSessionManager {
281    sessions: RiShardedLock<String, RiSession>,
282    timeout_secs: u64,
283    max_sessions_per_user: usize,
284}
285
286impl RiSessionManager {
287    /// Creates a new session manager with the specified timeout.
288    ///
289    /// # Parameters
290    ///
291    /// - `timeout_secs`: Default session timeout in seconds
292    ///
293    /// # Returns
294    ///
295    /// A new instance of `RiSessionManager`
296    ///
297    /// # Notes
298    ///
299    /// Default maximum sessions per user is 5
300    pub fn new(timeout_secs: u64) -> Self {
301        Self {
302            sessions: RiShardedLock::with_default_shards(),
303            timeout_secs,
304            max_sessions_per_user: 5,
305        }
306    }
307
308    /// Creates a new session for a user.
309    /// 
310    /// # Parameters
311    /// - `user_id`: ID of the user to create the session for
312    /// - `ip_address`: Optional client IP address
313    /// - `user_agent`: Optional client user agent
314    /// 
315    /// # Returns
316    /// The ID of the newly created session
317    /// 
318    /// # Notes
319    /// - If the user has reached the maximum number of sessions, the oldest session is removed
320    /// - Session ID is generated using UUID v4 for security
321    pub async fn create_session(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> crate::core::RiResult<String> {
322        let user_sessions: Vec<(String, u64)> = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await
323            .into_iter()
324            .map(|s| (s.id.clone(), s.created_at))
325            .collect();
326        
327        if user_sessions.len() >= self.max_sessions_per_user {
328            let mut sessions_with_time = user_sessions;
329            sessions_with_time.sort_by_key(|(_, t)| *t);
330            
331            if let Some((oldest_id, _)) = sessions_with_time.first() {
332                self.sessions.remove(oldest_id).await;
333            }
334        }
335
336        let session = RiSession::new(user_id, self.timeout_secs, ip_address, user_agent);
337        let session_id = session.id.clone();
338        self.sessions.insert(session_id.clone(), session).await;
339        
340        Ok(session_id)
341    }
342
343    /// Validates session security by checking IP and User-Agent.
344    ///
345    /// # Security
346    ///
347    /// This method helps prevent session hijacking by verifying that
348    /// the client's IP address and User-Agent match the original values.
349    ///
350    /// # Parameters
351    /// - `session_id`: ID of the session to validate
352    /// - `current_ip`: Current client IP address
353    /// - `current_user_agent`: Current client User-Agent
354    ///
355    /// # Returns
356    /// `true` if the session is valid, `false` if there's a mismatch
357    pub async fn validate_session_security(
358        &self,
359        session_id: &str,
360        current_ip: Option<&str>,
361        current_user_agent: Option<&str>,
362    ) -> crate::core::RiResult<bool> {
363        let session = self.sessions.get(session_id).await;
364        
365        match session {
366            Some(s) => {
367                if s.is_expired() {
368                    self.sessions.remove(session_id).await;
369                    return Ok(false);
370                }
371
372                // Check IP address match (if both are set)
373                if let (Some(stored_ip), Some(current)) = (&s.ip_address, current_ip) {
374                    if stored_ip != current {
375                        log::warn!(
376                            "[Ri.Session] IP address mismatch for session {}: expected {}, got {}",
377                            session_id, stored_ip, current
378                        );
379                        return Ok(false);
380                    }
381                }
382
383                // Check User-Agent match (if both are set)
384                if let (Some(stored_ua), Some(current)) = (&s.user_agent, current_user_agent) {
385                    if stored_ua != current {
386                        log::warn!(
387                            "[Ri.Session] User-Agent mismatch for session {}: expected {}, got {}",
388                            session_id, stored_ua, current
389                        );
390                        return Ok(false);
391                    }
392                }
393
394                Ok(true)
395            }
396            None => Ok(false),
397        }
398    }
399
400    /// Regenerates a session ID to prevent session fixation attacks.
401    ///
402    /// # Security
403    ///
404    /// This method should be called after successful authentication to
405    /// prevent session fixation attacks. It creates a new session ID
406    /// while preserving the session data.
407    ///
408    /// # Parameters
409    /// - `old_session_id`: The current session ID
410    ///
411    /// # Returns
412    /// The new session ID, or None if the old session doesn't exist
413    pub async fn regenerate_session_id(&self, old_session_id: &str) -> crate::core::RiResult<Option<String>> {
414        let old_session = self.sessions.get(old_session_id).await;
415        
416        match old_session {
417            Some(mut s) => {
418                if s.is_expired() {
419                    self.sessions.remove(old_session_id).await;
420                    return Ok(None);
421                }
422
423                // Remove old session
424                self.sessions.remove(old_session_id).await;
425
426                // Generate new session ID
427                let new_session_id = Uuid::new_v4().to_string();
428                
429                // Update session with new ID
430                s.id = new_session_id.clone();
431                s.touch();
432                
433                // Insert with new ID
434                self.sessions.insert(new_session_id.clone(), s).await;
435
436                log::info!(
437                    "[Ri.Session] Regenerated session ID: {} -> {}",
438                    old_session_id, new_session_id
439                );
440
441                Ok(Some(new_session_id))
442            }
443            None => Ok(None),
444        }
445    }
446
447    /// Gets a session by ID.
448    /// 
449    /// # Parameters
450    /// - `session_id`: ID of the session to retrieve
451    /// 
452    /// # Returns
453    /// `Some(RiSession)` if the session exists and is not expired, otherwise `None`
454    /// 
455    /// # Notes
456    /// - Expired sessions are automatically removed and return `None`
457    /// - The session's last accessed time is updated when retrieved
458    pub async fn get_session(&self, session_id: &str) -> crate::core::RiResult<Option<RiSession>> {
459        let session = self.sessions.get(session_id).await;
460        
461        match session {
462            Some(mut s) => {
463                if s.is_expired() {
464                    self.sessions.remove(session_id).await;
465                    Ok(None)
466                } else {
467                    s.touch();
468                    self.sessions.insert(session_id.to_string(), s.clone()).await;
469                    Ok(Some(s))
470                }
471            }
472            None => Ok(None),
473        }
474    }
475
476    /// Updates a session's data.
477    /// 
478    /// # Parameters
479    /// - `session_id`: ID of the session to update
480    /// - `data`: HashMap of key-value pairs to update in the session
481    /// 
482    /// # Returns
483    /// `true` if the session was updated successfully, `false` if the session doesn't exist or is expired
484    /// 
485    /// # Notes
486    /// - The session's last accessed time is updated when modified
487    pub async fn update_session(&self, session_id: &str, data: FxHashMap<String, String>) -> crate::core::RiResult<bool> {
488        let session = self.sessions.get(session_id).await;
489        
490        match session {
491            Some(mut s) => {
492                if s.is_expired() {
493                    self.sessions.remove(session_id).await;
494                    Ok(false)
495                } else {
496                    for (key, value) in data {
497                        s.set_data(key, value);
498                    }
499                    s.touch();
500                    self.sessions.insert(session_id.to_string(), s).await;
501                    Ok(true)
502                }
503            }
504            None => Ok(false),
505        }
506    }
507
508    /// Extends a session's expiration time.
509    /// 
510    /// # Parameters
511    /// - `session_id`: ID of the session to extend
512    /// 
513    /// # Returns
514    /// `true` if the session was extended successfully, `false` if the session doesn't exist or is expired
515    pub async fn extend_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
516        let session = self.sessions.get(session_id).await;
517        
518        match session {
519            Some(mut s) => {
520                if s.is_expired() {
521                    self.sessions.remove(session_id).await;
522                    Ok(false)
523                } else {
524                    s.extend(self.timeout_secs);
525                    self.sessions.insert(session_id.to_string(), s).await;
526                    Ok(true)
527                }
528            }
529            None => Ok(false),
530        }
531    }
532
533    /// Destroys a session by ID.
534    /// 
535    /// # Parameters
536    /// - `session_id`: ID of the session to destroy
537    /// 
538    /// # Returns
539    /// `true` if the session was destroyed successfully, `false` if the session doesn't exist
540    pub async fn destroy_session(&self, session_id: &str) -> crate::core::RiResult<bool> {
541        Ok(self.sessions.remove(session_id).await.is_some())
542    }
543
544    /// Destroys all sessions for a user.
545    /// 
546    /// # Parameters
547    /// - `user_id`: ID of the user whose sessions to destroy
548    /// 
549    /// # Returns
550    /// The number of sessions destroyed
551    pub async fn destroy_user_sessions(&self, user_id: &str) -> crate::core::RiResult<usize> {
552        let count = self.sessions.remove_where(|_, s| s.user_id == user_id).await;
553        Ok(count)
554    }
555
556    /// Gets all active sessions for a user.
557    /// 
558    /// # Parameters
559    /// - `user_id`: ID of the user whose sessions to retrieve
560    /// 
561    /// # Returns
562    /// A vector of active sessions for the user
563    pub async fn get_user_sessions(&self, user_id: &str) -> crate::core::RiResult<Vec<RiSession>> {
564        let user_sessions = self.sessions.collect_where(|_, s| s.user_id == user_id && !s.is_expired()).await;
565        Ok(user_sessions)
566    }
567
568    /// Cleans up all expired sessions.
569    /// 
570    /// # Returns
571    /// The number of expired sessions cleaned up
572    pub async fn cleanup_expired(&self) -> crate::core::RiResult<usize> {
573        let count = self.sessions.remove_where(|_, s| s.is_expired()).await;
574        Ok(count)
575    }
576
577    /// Cleans up all sessions.
578    /// 
579    /// This method removes all sessions, regardless of their expiration status.
580    pub async fn cleanup_all(&self) -> crate::core::RiResult<()> {
581        self.sessions.clear().await;
582        Ok(())
583    }
584
585    /// Gets the default session timeout.
586    /// 
587    /// # Returns
588    /// The default session timeout in seconds
589    pub fn get_timeout(&self) -> u64 {
590        self.timeout_secs
591    }
592
593    /// Sets the default session timeout.
594    /// 
595    /// # Parameters
596    /// - `timeout_secs`: New default session timeout in seconds
597    pub fn set_timeout(&mut self, timeout_secs: u64) {
598        self.timeout_secs = timeout_secs;
599    }
600}
601
602#[cfg(feature = "pyo3")]
603/// Python bindings for the Session Manager.
604///
605/// This module provides Python interface to Ri session management functionality,
606/// enabling Python applications to manage user sessions with expiration and data storage.
607///
608/// ## Supported Operations
609///
610/// - Session creation with user ID, IP address, and user agent tracking
611/// - Session retrieval and validation
612/// - Session data storage with key-value pairs
613/// - Session expiration management
614/// - Session cleanup for expired sessions
615///
616/// ## Python Usage Example
617///
618/// ```python
619/// from ri import RiSessionManager
620///
621/// # Create session manager with 30-minute timeout
622/// session_manager = RiSessionManager(1800)
623///
624/// # Create a new session
625/// session_id = session_manager.create_session(
626///     "user123",
627///     "192.168.1.1",
628///     "Mozilla/5.0"
629/// )
630///
631/// # Get session data
632/// session = session_manager.get_session(session_id)
633/// if session:
634///     print(f"Session created at: {session.created_at}")
635///     print(f"Session expires at: {session.expires_at}")
636///
637/// # Update session data
638/// session_manager.update_session(session_id, {"theme": "dark"})
639///
640/// # Extend session
641/// session_manager.extend_session(session_id)
642///
643/// # Destroy session when done
644/// session_manager.destroy_session(session_id)
645/// ```
646///
647/// ## Limitations
648///
649/// The current Python bindings do not support async session operations.
650/// For async scenarios, use the Rust API directly or implement async wrappers
651/// using Python's asyncio library.
652#[pyo3::prelude::pymethods]
653impl RiSessionManager {
654    #[new]
655    fn py_new(timeout_secs: u64) -> PyResult<Self> {
656        Ok(Self::new(timeout_secs))
657    }
658    
659    #[pyo3(name = "create_session")]
660    fn create_session_impl(&self, user_id: String, ip_address: Option<String>, user_agent: Option<String>) -> PyResult<String> {
661        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
662        rt.block_on(async {
663            self.create_session(user_id, ip_address, user_agent).await
664                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
665        })
666    }
667    
668    #[pyo3(name = "get_session")]
669    fn get_session_impl(&self, session_id: String) -> PyResult<Option<RiSession>> {
670        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
671        rt.block_on(async {
672            self.get_session(&session_id).await
673                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
674        })
675    }
676    
677    #[pyo3(name = "update_session")]
678    fn update_session_impl(&self, session_id: String, data: FxHashMap<String, String>) -> PyResult<bool> {
679        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
680        rt.block_on(async {
681            self.update_session(&session_id, data).await
682                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
683        })
684    }
685    
686    #[pyo3(name = "destroy_session")]
687    fn destroy_session_impl(&self, session_id: String) -> PyResult<bool> {
688        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
689        rt.block_on(async {
690            self.destroy_session(&session_id).await
691                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
692        })
693    }
694    
695    #[pyo3(name = "extend_session")]
696    fn extend_session_impl(&self, session_id: String) -> PyResult<bool> {
697        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
698        rt.block_on(async {
699            self.extend_session(&session_id).await
700                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
701        })
702    }
703    
704    #[pyo3(name = "cleanup_expired")]
705    fn cleanup_expired_impl(&self) -> PyResult<usize> {
706        let rt = tokio::runtime::Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
707        rt.block_on(async {
708            self.cleanup_expired().await
709                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
710        })
711    }
712    
713    #[pyo3(name = "get_timeout")]
714    fn get_timeout_impl(&self) -> u64 {
715        self.get_timeout()
716    }
717}