thetime/
ntp.rs

1use chrono::{DateTime, NaiveDateTime, Utc};
2use core::fmt::Display;
3use std::net::UdpSocket;
4use core::time::Duration;
5use serde::{Deserialize, Serialize};
6
7use crate::{Time, TimeDiff, OFFSET_1601, REF_TIME_1970};
8
9/// NTP time
10///
11/// `inner_secs` is the time as seconds since `1601-01-01 00:00:00`, from `chrono::Utc`
12/// `inner_milliseconds` is the subsec milliseconds
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub struct Ntp {
15    inner_secs: u64,
16    inner_milliseconds: u64,
17    server: String,
18    utc_offset: i32,
19}
20
21impl Display for Ntp {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        write!(f, "{}", self.pretty())
24    }
25}
26
27impl Ntp {
28    /// Returns the server address used to get the time
29    pub fn server(&self) -> String {
30        self.server.to_string()
31    }
32
33    /// returns whether the data was fetched from a valid server (ie not strptime or chrono::Utc)
34    pub fn valid_server(&self) -> bool {
35        !["chrono::Utc", "strptime"].contains(&self.server.as_str())
36    }
37}
38
39impl TimeDiff for Ntp {}
40
41impl Time for Ntp {
42    /// Note - there is a chance that this function fails, in which case we use the System time as a failsafe
43    fn now() -> Self {
44        match Ntp::new("pool.ntp.org") {
45            Ok(x) => x,
46            Err(_) => {
47                let now = Utc::now();
48                Ntp {
49                    inner_secs: (now.timestamp() + OFFSET_1601 as i64) as u64,
50                    inner_milliseconds: now.timestamp_subsec_millis() as u64,
51                    server: "chrono::Utc".to_string(),
52                    utc_offset: 0,
53                }
54            },
55        }
56    }
57    fn unix(&self) -> i64 {
58        (self.inner_secs as i64) - (OFFSET_1601 as i64)
59    }
60    fn unix_ms(&self) -> i64 {
61        ((self.inner_secs as i64 * 1000i64) + self.inner_milliseconds as i64) - (OFFSET_1601 as i64 * 1000i64)
62    }
63    fn utc_offset(&self) -> i32 {
64        self.utc_offset
65    }
66
67    fn strptime<T: ToString, G: ToString>(s: T, format: G) -> Self {
68        let s = s.to_string();
69        let format = format.to_string();
70        let temp = DateTime::parse_from_str(&s, &format);
71        let x = match temp {
72            Err(_) => {
73                if !format.contains("%z") {
74                    return Self::strptime(s + " +0000", format + "%z");
75                }
76                panic!("Bad format string");
77            }
78            Ok(dt) => dt,
79        };
80        Ntp {
81            inner_secs: (x.timestamp() + (OFFSET_1601 as i64)) as u64,
82            inner_milliseconds: x.timestamp_subsec_millis() as u64,
83            server: "strptime".to_string(),
84            utc_offset: x.offset().local_minus_utc() as i32,
85        }
86    }
87
88    fn strftime(&self, format: &str) -> String {
89        NaiveDateTime::from_timestamp_opt(self.inner_secs as i64 - OFFSET_1601 as i64, 0)
90            .unwrap()
91            .format(format)
92            .to_string()
93    }
94
95    fn from_epoch(timestamp: u64) -> Self {
96        Ntp {
97            inner_secs: timestamp / 1000,
98            inner_milliseconds: timestamp % 1000,
99            server: "from_epoch".to_string(),
100            utc_offset: 0,
101        }
102    }
103
104    fn raw(&self) -> u64 {
105        (self.inner_secs * 1000) + self.inner_milliseconds
106    }
107
108    fn from_epoch_offset(timestamp: u64, offset: i32) -> Self {
109        Ntp {
110            inner_secs: timestamp / 1000,
111            inner_milliseconds: timestamp % 1000,
112            server: "from_epoch_offset".to_string(),
113            utc_offset: offset,
114        }
115    }
116}
117
118
119impl Ntp {
120    /// Fetches the time from an NTP server
121    /// 
122    /// # Example
123    /// ```
124    /// use thetime::Ntp;
125    /// let ntp = Ntp::new("pool.ntp.org").unwrap();
126    /// println!("{}", ntp);
127    /// ```
128    pub fn new<T: ToString>(server_addr: T) -> Result<Ntp, Box<dyn std::error::Error>> {
129        let server = server_addr.to_string();
130        let client = UdpSocket::bind("0.0.0.0:0")?;
131        client.set_read_timeout(Some(Duration::from_secs(5)))?;
132    
133        let mut data = vec![0x1b];
134        data.extend(vec![0; 47]); // ping
135    
136        let start_time = Utc::now().timestamp_millis();
137        client.send_to(&data, format!("{}:123", server))?;
138    
139        let mut buffer = [0; 1024];
140        let (size, _) = client.recv_from(&mut buffer)?;
141    
142        if size > 0 {
143            let t = u32::from_be_bytes([buffer[40], buffer[41], buffer[42], buffer[43]]) as u64;
144            let t = t - REF_TIME_1970;
145    
146            let elapsed_time = start_time - Utc::now().timestamp_millis();
147            let milliseconds = (elapsed_time % 1000).try_into().unwrap_or(0);
148    
149            return Ok(Ntp {
150                server: server.to_string(),
151                inner_secs: (t) + OFFSET_1601,
152                inner_milliseconds: milliseconds,
153                utc_offset: 0,
154            });
155        }
156    
157        Err("Failed to receive NTP response".into())
158    }
159    
160}