1use std::io::{Read, Write};
14use std::net::{SocketAddr, TcpStream};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18use crate::net::{self, Net, Target};
19
20pub const MAX_BODY: usize = 256 * 1024;
22pub const MAX_REDIRECTS: usize = 5;
24
25#[derive(Clone)]
27pub struct HttpProbe {
28 pub url: String,
29 pub method: String,
31 pub headers: Vec<(String, String)>,
32 pub timeout: Duration,
33 pub follow_redirects: bool,
34 pub allow_private: bool,
35 pub connect_to: Option<SocketAddr>,
39 pub tls: Arc<rustls::ClientConfig>,
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct HttpAnswer {
45 pub status: u16,
46 pub location: Option<String>,
47 pub body: Vec<u8>,
49 pub latency: Duration,
50 pub cert_expires: Option<u64>,
52 pub final_url: String,
54}
55
56pub fn display_url(url: &str) -> String {
58 let u = url.split(['?', '#']).next().unwrap_or_default();
59 u.to_string()
60}
61
62pub fn http(p: &HttpProbe) -> Result<HttpAnswer, String> {
64 let deadline = Instant::now() + p.timeout;
65 let started = Instant::now();
66 let mut url = p.url.clone();
67 for hop in 0..=MAX_REDIRECTS {
68 let t = net::parse_url(&url).map_err(|e| format!("URL: {e}"))?;
69 let connect_to = if hop == 0 { p.connect_to } else { None };
71 let mut a = once(p, &t, connect_to, deadline)?;
72 a.final_url = display_url(&url);
73 let redirect = (300..400).contains(&a.status) && a.status != 304;
74 match (&a.location, redirect && p.follow_redirects) {
75 (Some(loc), true) if hop < MAX_REDIRECTS => url = join(&t, loc),
76 _ => {
77 a.latency = started.elapsed();
78 return Ok(a);
79 }
80 }
81 }
82 Err(format!("more than {MAX_REDIRECTS} redirects"))
83}
84
85pub fn join(base: &Target, loc: &str) -> String {
87 let l = loc.trim();
88 if l.starts_with("http://") || l.starts_with("https://") {
89 return l.to_string();
90 }
91 let scheme = if base.https { "https" } else { "http" };
92 let default = if base.https { 443 } else { 80 };
93 let host = if base.host.contains(':') {
94 format!("[{}]", base.host)
95 } else {
96 base.host.clone()
97 };
98 let authority = if base.port == default {
99 host
100 } else {
101 format!("{host}:{}", base.port)
102 };
103 if let Some(rest) = l.strip_prefix("//") {
104 return format!("{scheme}://{rest}");
105 }
106 if l.starts_with('/') {
107 return format!("{scheme}://{authority}{l}");
108 }
109 let dir = base.path.split('?').next().unwrap_or("/");
110 let dir = &dir[..dir.rfind('/').map(|i| i + 1).unwrap_or(1)];
111 format!("{scheme}://{authority}{dir}{l}")
112}
113
114fn left(deadline: Instant) -> Result<Duration, String> {
115 let d = deadline.saturating_duration_since(Instant::now());
116 if d.is_zero() {
117 Err("timed out".into())
118 } else {
119 Ok(d.max(Duration::from_millis(1)))
120 }
121}
122
123pub fn connect(
125 host: &str,
126 port: u16,
127 allow_private: bool,
128 deadline: Instant,
129) -> Result<TcpStream, String> {
130 let addrs = net::resolve(host, port, allow_private).map_err(|e| e.message)?;
131 let mut last = None;
132 for a in addrs {
133 match dial(a, deadline) {
134 Ok(s) => {
135 let peer = s.peer_addr().map_err(|e| format!("{host}: {e}"))?;
136 net::check_ip(peer.ip(), allow_private)
137 .map_err(|why| format!("refusing {host}: {why}"))?;
138 return Ok(s);
139 }
140 Err(e) => last = Some(e),
141 }
142 }
143 Err(format!(
144 "cannot connect to {host}:{port}: {}",
145 last.unwrap_or_default()
146 ))
147}
148
149pub fn dial(a: SocketAddr, deadline: Instant) -> Result<TcpStream, String> {
151 let s = TcpStream::connect_timeout(&a, left(deadline)?).map_err(|e| {
152 if e.kind() == std::io::ErrorKind::TimedOut || e.kind() == std::io::ErrorKind::WouldBlock {
153 "timed out connecting".to_string()
154 } else {
155 e.to_string()
156 }
157 })?;
158 s.set_nodelay(true).ok();
159 Ok(s)
160}
161
162pub fn tcp(
164 host: &str,
165 port: u16,
166 timeout: Duration,
167 allow_private: bool,
168) -> Result<Duration, String> {
169 let started = Instant::now();
170 let s = connect(host, port, allow_private, started + timeout)?;
171 drop(s);
172 Ok(started.elapsed())
173}
174
175fn host_header(t: &Target) -> String {
176 let h = if t.host.contains(':') {
177 format!("[{}]", t.host)
178 } else {
179 t.host.clone()
180 };
181 let default = if t.https { 443 } else { 80 };
182 if t.port == default {
183 h
184 } else {
185 format!("{h}:{}", t.port)
186 }
187}
188
189fn request_head(p: &HttpProbe, t: &Target) -> Result<String, String> {
190 let mut head = format!(
191 "{} {} HTTP/1.1\r\nHost: {}\r\nUser-Agent: isb-monitor/{}\r\nAccept: */*\r\nConnection: close\r\n",
192 p.method,
193 t.path,
194 host_header(t),
195 env!("CARGO_PKG_VERSION"),
196 );
197 for (k, v) in &p.headers {
198 if k.contains(['\r', '\n', ':']) || v.contains(['\r', '\n']) {
199 return Err(format!("a bad header {k:?}"));
200 }
201 head.push_str(&format!("{k}: {v}\r\n"));
202 }
203 head.push_str("\r\n");
204 Ok(head)
205}
206
207fn once(
209 p: &HttpProbe,
210 t: &Target,
211 connect_to: Option<SocketAddr>,
212 deadline: Instant,
213) -> Result<HttpAnswer, String> {
214 let head = request_head(p, t)?;
215 let tcp = match connect_to {
216 Some(a) => dial(a, deadline).map_err(|e| format!("cannot connect to {a}: {e}"))?,
217 None => connect(&t.host, t.port, p.allow_private, deadline)?,
218 };
219 let head_only = p.method == "HEAD";
220 if !t.https {
221 let sock = s_clone(&tcp)?;
222 let mut s = tcp;
223 let raw = exchange(&mut s, &sock, head.as_bytes(), deadline, &t.host)?;
224 return parse(&raw, head_only);
225 }
226 let net = Net {
227 allow_private: p.allow_private,
228 tls: p.tls.clone(),
229 };
230 let mut s = net::tls(&net, &t.host, tcp).map_err(|e| e.message)?;
231 let sock = s_clone(&s.sock)?;
232 while s.conn.is_handshaking() {
233 sock.set_read_timeout(Some(left(deadline)?)).ok();
234 s.conn
235 .complete_io(&mut s.sock)
236 .map_err(|e| tls_error(&t.host, &e))?;
237 }
238 let cert_expires = s
239 .conn
240 .peer_certificates()
241 .and_then(|c| c.first())
242 .and_then(|c| cert_not_after(c.as_ref()));
243 let raw = exchange(&mut s, &sock, head.as_bytes(), deadline, &t.host)?;
244 let mut a = parse(&raw, head_only)?;
245 a.cert_expires = cert_expires;
246 Ok(a)
247}
248
249fn s_clone(s: &TcpStream) -> Result<TcpStream, String> {
250 s.try_clone().map_err(|e| e.to_string())
251}
252
253fn tls_error(host: &str, e: &std::io::Error) -> String {
254 let m = e.to_string();
255 if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut {
256 format!("{host}: timed out in the TLS handshake")
257 } else {
258 format!("TLS to {host}: {m}")
259 }
260}
261
262fn exchange<S: Read + Write>(
265 s: &mut S,
266 sock: &TcpStream,
267 head: &[u8],
268 deadline: Instant,
269 host: &str,
270) -> Result<Vec<u8>, String> {
271 let io = |e: std::io::Error| {
272 if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut {
273 format!("{host}: timed out waiting for an answer")
274 } else {
275 format!("{host}: {e}")
276 }
277 };
278 sock.set_write_timeout(Some(left(deadline)?)).ok();
279 s.write_all(head).map_err(io)?;
280 s.flush().map_err(io)?;
281 let mut out = Vec::new();
282 let mut buf = [0u8; 16384];
283 loop {
284 sock.set_read_timeout(Some(
285 left(deadline).map_err(|_| format!("{host}: timed out waiting for an answer"))?,
286 ))
287 .ok();
288 match s.read(&mut buf) {
289 Ok(0) => break,
290 Ok(n) => {
291 out.extend_from_slice(&buf[..n]);
292 if out.len() >= MAX_BODY + 16384 || complete(&out) {
293 break;
294 }
295 }
296 Err(_) if header_end(&out).is_some() => break,
297 Err(e) => return Err(io(e)),
298 }
299 }
300 if out.is_empty() {
301 return Err(format!("{host}: closed the connection without answering"));
302 }
303 Ok(out)
304}
305
306fn header_end(b: &[u8]) -> Option<usize> {
307 b.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
308}
309
310fn complete(b: &[u8]) -> bool {
313 let Some(end) = header_end(b) else {
314 return false;
315 };
316 let mut headers = [httparse::EMPTY_HEADER; 64];
317 let mut r = httparse::Response::new(&mut headers);
318 if r.parse(b).is_err() {
319 return false;
320 }
321 let header = |name: &str| {
322 r.headers
323 .iter()
324 .find(|h| h.name.eq_ignore_ascii_case(name))
325 .and_then(|h| std::str::from_utf8(h.value).ok())
326 .map(|v| v.trim().to_ascii_lowercase())
327 };
328 if header("transfer-encoding").is_some_and(|v| v.contains("chunked")) {
329 return b[end..].ends_with(b"0\r\n\r\n");
330 }
331 match header("content-length").and_then(|v| v.parse::<usize>().ok()) {
332 Some(n) => b.len() >= end + n,
333 None => false,
334 }
335}
336
337pub fn parse(raw: &[u8], head_only: bool) -> Result<HttpAnswer, String> {
339 let mut headers = [httparse::EMPTY_HEADER; 64];
340 let mut r = httparse::Response::new(&mut headers);
341 let end = match r.parse(raw) {
342 Ok(httparse::Status::Complete(n)) => n,
343 Ok(httparse::Status::Partial) => return Err("an incomplete HTTP answer".into()),
344 Err(e) => return Err(format!("not an HTTP answer: {e}")),
345 };
346 let header = |name: &str| {
347 r.headers
348 .iter()
349 .find(|h| h.name.eq_ignore_ascii_case(name))
350 .and_then(|h| std::str::from_utf8(h.value).ok())
351 .map(|v| v.trim().to_string())
352 };
353 let chunked =
354 header("transfer-encoding").is_some_and(|v| v.to_ascii_lowercase().contains("chunked"));
355 let raw_body = if head_only { &[][..] } else { &raw[end..] };
356 let mut body = if chunked {
357 dechunk(raw_body)
358 } else {
359 raw_body.to_vec()
360 };
361 body.truncate(MAX_BODY);
362 Ok(HttpAnswer {
363 status: r.code.unwrap_or(0),
364 location: header("location"),
365 body,
366 ..Default::default()
367 })
368}
369
370pub fn dechunk(mut b: &[u8]) -> Vec<u8> {
372 let mut out = Vec::new();
373 while let Some(i) = b.windows(2).position(|w| w == b"\r\n") {
374 let size = std::str::from_utf8(&b[..i])
375 .ok()
376 .and_then(|s| usize::from_str_radix(s.split(';').next()?.trim(), 16).ok());
377 let Some(n) = size else { break };
378 if n == 0 {
379 break;
380 }
381 let start = i + 2;
382 let stop = (start + n).min(b.len());
383 out.extend_from_slice(&b[start..stop]);
384 if start + n + 2 > b.len() {
385 break;
386 }
387 b = &b[start + n + 2..];
388 }
389 out
390}
391
392pub fn cert_not_after(der: &[u8]) -> Option<u64> {
397 let (_, cert, _) = tlv(der)?;
398 let (_, tbs, _) = tlv(cert)?;
399 let mut rest = tbs;
400 let (tag, _, r) = tlv(rest)?;
401 if tag == 0xa0 {
402 rest = r; }
404 for _ in 0..3 {
405 rest = tlv(rest)?.2; }
407 let (tag, validity, _) = tlv(rest)?;
408 if tag != 0x30 {
409 return None;
410 }
411 let (_, _, r) = tlv(validity)?; let (tag, t, _) = tlv(r)?;
413 let s = std::str::from_utf8(t).ok()?;
414 match tag {
415 0x17 => asn1_time(s, true),
416 0x18 => asn1_time(s, false),
417 _ => None,
418 }
419}
420
421fn tlv(b: &[u8]) -> Option<(u8, &[u8], &[u8])> {
423 let tag = *b.first()?;
424 let first = *b.get(1)?;
425 let (len, hdr) = if first < 0x80 {
426 (first as usize, 2)
427 } else {
428 let n = (first & 0x7f) as usize;
429 if n == 0 || n > 4 {
430 return None;
431 }
432 let mut len = 0usize;
433 for i in 0..n {
434 len = (len << 8) | *b.get(2 + i)? as usize;
435 }
436 (len, 2 + n)
437 };
438 let end = hdr.checked_add(len)?;
439 (end <= b.len()).then(|| (tag, &b[hdr..end], &b[end..]))
440}
441
442fn asn1_time(s: &str, utc: bool) -> Option<u64> {
444 let s = s.strip_suffix('Z')?;
445 let (year, rest) = if utc {
446 let y: i64 = s.get(..2)?.parse().ok()?;
447 (if y >= 50 { 1900 + y } else { 2000 + y }, s.get(2..)?)
448 } else {
449 (s.get(..4)?.parse().ok()?, s.get(4..)?)
450 };
451 let n = |i: usize| -> Option<i64> { rest.get(i..i + 2)?.parse().ok() };
452 let (mo, d, h, mi, se) = (n(0)?, n(2)?, n(4)?, n(6)?, n(8)?);
453 let days = days_from_civil(year, mo, d);
454 let t = days * 86400 + h * 3600 + mi * 60 + se;
455 u64::try_from(t).ok()
456}
457
458pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
460 let y = if m <= 2 { y - 1 } else { y };
461 let era = y.div_euclid(400);
462 let yoe = y - era * 400;
463 let mp = (m + 9) % 12;
464 let doy = (153 * mp + 2) / 5 + d - 1;
465 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
466 era * 146_097 + doe - 719_468
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use std::net::TcpListener;
473
474 fn probe(url: &str) -> HttpProbe {
475 HttpProbe {
476 url: url.into(),
477 method: "GET".into(),
478 headers: vec![("X-Token".into(), "t0k".into())],
479 timeout: Duration::from_secs(3),
480 follow_redirects: false,
481 allow_private: true,
482 connect_to: None,
483 tls: net::default_tls(),
484 }
485 }
486
487 fn server(answers: Vec<String>) -> (u16, std::thread::JoinHandle<Vec<String>>) {
490 let l = TcpListener::bind("127.0.0.1:0").unwrap();
491 let port = l.local_addr().unwrap().port();
492 let h = std::thread::spawn(move || {
493 let mut got = Vec::new();
494 for a in answers {
495 let (mut s, _) = l.accept().unwrap();
496 let mut buf = vec![0u8; 8192];
497 let n = s.read(&mut buf).unwrap();
498 got.push(String::from_utf8_lossy(&buf[..n]).to_string());
499 s.write_all(a.as_bytes()).unwrap();
500 }
501 got
502 });
503 (port, h)
504 }
505
506 #[test]
507 fn get_with_headers_and_a_chunked_body() {
508 let (port, h) = server(vec![
509 "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n"
510 .into(),
511 ]);
512 let a = http(&probe(&format!("http://127.0.0.1:{port}/health?k=1"))).unwrap();
513 assert_eq!(a.status, 200);
514 assert_eq!(a.body, b"hello world");
515 assert_eq!(a.final_url, format!("http://127.0.0.1:{port}/health"));
516 let req = &h.join().unwrap()[0];
517 assert!(req.starts_with("GET /health?k=1 HTTP/1.1\r\n"), "{req}");
518 assert!(req.contains("X-Token: t0k\r\n"), "{req}");
519 }
520
521 #[test]
522 fn redirects_are_followed_only_when_asked() {
523 let (port, h) = server(vec![
524 "HTTP/1.1 302 Found\r\nLocation: /login\r\nContent-Length: 0\r\n\r\n".into(),
525 "HTTP/1.1 302 Found\r\nLocation: /login\r\nContent-Length: 0\r\n\r\n".into(),
526 "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok".into(),
527 ]);
528 let url = format!("http://127.0.0.1:{port}/");
529 let a = http(&probe(&url)).unwrap();
530 assert_eq!((a.status, a.location.as_deref()), (302, Some("/login")));
531 let mut p = probe(&url);
532 p.follow_redirects = true;
533 let a = http(&p).unwrap();
534 assert_eq!((a.status, a.body.as_slice()), (200, &b"ok"[..]));
535 assert!(a.final_url.ends_with("/login"));
536 h.join().unwrap();
537 }
538
539 #[test]
540 fn the_address_policy_holds_unless_dialled_by_reference() {
541 let (port, h) = server(vec!["HTTP/1.1 204 No Content\r\n\r\n".into()]);
542 let mut p = probe(&format!("http://127.0.0.1:{port}/"));
543 p.allow_private = false;
544 let e = http(&p).unwrap_err();
545 assert!(e.contains("refusing 127.0.0.1"), "{e}");
546 let mut p = probe("http://shop.example.com/");
548 p.allow_private = false;
549 p.connect_to = Some(format!("127.0.0.1:{port}").parse().unwrap());
550 assert_eq!(http(&p).unwrap().status, 204);
551 assert!(h.join().unwrap()[0].contains("Host: shop.example.com\r\n"));
552 assert!(tcp("127.0.0.1", port, Duration::from_secs(1), false).is_err());
554 }
555
556 #[test]
557 fn timeouts_and_refusals() {
558 let l = TcpListener::bind("127.0.0.1:0").unwrap();
560 let port = l.local_addr().unwrap().port();
561 let mut p = probe(&format!("http://127.0.0.1:{port}/"));
562 p.timeout = Duration::from_millis(300);
563 let started = Instant::now();
564 let e = http(&p).unwrap_err();
565 assert!(e.contains("timed out"), "{e}");
566 assert!(started.elapsed() < Duration::from_secs(2));
567 drop(l);
568 let e = http(&probe(&format!("http://127.0.0.1:{port}/"))).unwrap_err();
569 assert!(e.contains("cannot connect"), "{e}");
570 let e = tcp("127.0.0.1", port, Duration::from_secs(1), true).unwrap_err();
571 assert!(e.contains("cannot connect"), "{e}");
572 }
573
574 #[test]
575 fn redirect_targets() {
576 let t = net::parse_url("https://a.example.com:8443/x/y?q").unwrap();
577 assert_eq!(join(&t, "/z"), "https://a.example.com:8443/z");
578 assert_eq!(join(&t, "z"), "https://a.example.com:8443/x/z");
579 assert_eq!(join(&t, "http://b.example/"), "http://b.example/");
580 assert_eq!(join(&t, "//c.example/p"), "https://c.example/p");
581 assert_eq!(display_url("https://a/b?token=1#f"), "https://a/b");
582 }
583
584 #[test]
585 fn certificate_expiry() {
586 let mut params = rcgen::CertificateParams::new(vec!["shop.example.com".into()]).unwrap();
587 params.not_after = rcgen::date_time_ymd(2031, 7, 9);
588 let key = rcgen::KeyPair::generate().unwrap();
589 let cert = params.self_signed(&key).unwrap();
590 let want = days_from_civil(2031, 7, 9) as u64 * 86400;
591 assert_eq!(cert_not_after(cert.der().as_ref()), Some(want));
592 params.not_after = rcgen::date_time_ymd(2051, 1, 2);
594 let cert = params.self_signed(&key).unwrap();
595 assert_eq!(
596 cert_not_after(cert.der().as_ref()),
597 Some(days_from_civil(2051, 1, 2) as u64 * 86400)
598 );
599 assert_eq!(cert_not_after(b"\x30\x03\x02\x01\x01"), None);
600 assert_eq!(cert_not_after(&[]), None);
601 assert_eq!(days_from_civil(1970, 1, 1), 0);
602 assert_eq!(days_from_civil(2000, 3, 1), 11017);
603 }
604}