1#[macro_use]
30extern crate error_chain;
31
32use error::{Error, ErrorKind, Result};
33use error_chain::State;
34use openssl::{
35 asn1::*,
36 ssl::{Ssl, SslContext, SslMethod, SslVerifyMode},
37};
38use openssl_sys::ASN1_TIME;
39use std::{
40 net::{TcpStream, ToSocketAddrs},
41 os::raw::c_int,
42 time::Duration,
43};
44
45
46extern "C" {
47 fn ASN1_TIME_diff(
48 pday: *mut c_int,
49 psec: *mut c_int,
50 from: *const ASN1_TIME,
51 to: *const ASN1_TIME,
52 );
53}
54
55
56pub struct SslExpiration(c_int);
57
58impl SslExpiration {
59 pub fn from_domain_name(domain: &str) -> Result<SslExpiration> {
63 SslExpiration::from_addr(format!("{}:443", domain), domain, 30) }
65
66 pub fn from_domain_name_with_timeout(domain: &str, timeout: u64) -> Result<SslExpiration> {
68 SslExpiration::from_addr(format!("{}:443", domain), domain, timeout)
69 }
70
71 #[link(name = "ssl")]
73 pub fn from_addr<A: ToSocketAddrs>(
74 addr: A,
75 domain: &str,
76 timeout: u64,
77 ) -> Result<SslExpiration> {
78 let context = {
79 let mut context = SslContext::builder(SslMethod::tls())?;
80 context.set_verify(SslVerifyMode::empty());
81 context.build()
82 };
83 let mut connector = Ssl::new(&context)?;
84 connector.set_hostname(domain)?;
85 match addr.to_socket_addrs()?.next() {
86 Some(first_address) => {
87 let stream =
88 TcpStream::connect_timeout(&first_address, Duration::from_secs(timeout))?;
89 stream.set_write_timeout(Some(Duration::from_secs(timeout)))?;
90 stream.set_read_timeout(Some(Duration::from_secs(timeout)))?;
91
92 let stream = connector
93 .connect(stream)
94 .map_err(|e| ErrorKind::HandshakeError(e.to_string()))?;
95 let cert = stream
96 .ssl()
97 .peer_certificate()
98 .ok_or("Certificate not found")?;
99
100 let now = Asn1Time::days_from_now(0)?;
101
102 let (mut pday, mut psec) = (0, 0);
103 let ptr_pday: *mut c_int = &mut pday;
104 let ptr_psec: *mut c_int = &mut psec;
105 let now_ptr = &now as *const _ as *const _;
106 let after_ptr = &cert.not_after() as *const _ as *const _;
107 unsafe {
108 ASN1_TIME_diff(ptr_pday, ptr_psec, *now_ptr, *after_ptr);
109 }
110
111 Ok(SslExpiration(pday * 24 * 60 * 60 + psec))
112 }
113 None => {
114 Err(Error(
115 ErrorKind::HandshakeError(format!(
116 "Couldn't resolve any address from domain: {}",
117 &domain
118 )),
119 State::default(),
120 ))
121 }
122 }
123 }
124
125 pub fn secs(&self) -> i32 {
129 self.0
130 }
131
132 pub fn days(&self) -> i32 {
136 self.0 / 60 / 60 / 24
137 }
138
139 pub fn is_expired(&self) -> bool {
141 self.0 < 0
142 }
143}
144
145pub mod error {
146 use std::io;
147
148 error_chain! {
149 foreign_links {
150 OpenSslErrorStack(openssl::error::ErrorStack);
151 IoError(io::Error);
152 }
153 errors {
154 HandshakeError(e: String) {
155 display("HandshakeError: {}", e)
156 }
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165
166 #[test]
167 fn test_ssl_not_expired() {
168 assert!(
169 !SslExpiration::from_domain_name("google.com")
170 .unwrap()
171 .is_expired()
172 );
173 let days = SslExpiration::from_domain_name("google.com")
174 .unwrap()
175 .days();
176 assert!(days > 14)
177 }
178
179
180 #[test]
181 fn test_too_small_timeout_chain() {
182 SslExpiration::from_domain_name_with_timeout("google.com", 0)
183 .and_then(|_| Ok(assert!(false)))
184 .unwrap_or_else(|_| assert!(true));
185 }
186
187
188 #[test]
189 fn test_unresolvable_timeout_chain() {
190 SslExpiration::from_domain_name_with_timeout("unresolvable.net", 3)
191 .and_then(|_| Ok(assert!(false)))
192 .map_err(|e| println!("Error: {:?}", e))
193 .unwrap_or_else(|_| assert!(true));
194 }
195
196
197 #[test]
198 fn test_sufficient_timeout_chain() {
199 SslExpiration::from_domain_name_with_timeout("google.com", 30)
200 .and_then(|_| Ok(assert!(true)))
201 .unwrap_or_else(|_| assert!(false));
202 }
203
204
205 #[test]
206 fn test_non_panicing_chain() {
207 SslExpiration::from_domain_name("google.com")
208 .and_then(|validity| Ok(assert!(validity.days() > 14)))
209 .unwrap_or_else(|_| assert!(false));
210 }
211
212
213 #[test]
214 fn test_ssl_expired() {
215 assert!(
216 SslExpiration::from_domain_name("expired.identrustssl.com")
217 .unwrap()
218 .is_expired()
219 );
220 }
221}