strand_version_check/
lib.rs1use std::time::Duration;
19
20use bytes::Bytes;
21use http_body_util::{combinators::BoxBody, BodyExt, Full};
22use hyper_rustls::HttpsConnector;
23use hyper_util::{
24 client::legacy::{connect::HttpConnector, Client},
25 rt::TokioExecutor,
26};
27use serde::Deserialize;
28use tracing::warn;
29
30type Body = BoxBody<Bytes, std::convert::Infallible>;
31
32fn empty_body() -> Body {
33 Full::new(Bytes::new())
34 .map_err(|never| match never {})
35 .boxed()
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct AvailableVersion {
41 pub version: semver::Version,
43 pub message: String,
45 pub url: String,
47}
48
49#[derive(Clone)]
54pub struct VersionChecker {
55 client: Client<HttpsConnector<HttpConnector>, Body>,
56}
57
58impl Default for VersionChecker {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl VersionChecker {
65 pub fn new() -> Self {
67 let https = hyper_rustls::HttpsConnectorBuilder::new()
68 .with_webpki_roots()
69 .https_only()
70 .enable_http1()
71 .build();
72 let client = Client::builder(TokioExecutor::new()).build::<_, Body>(https);
73 Self { client }
74 }
75
76 pub async fn fetch(&self, product: &str, user_agent: &str) -> Option<AvailableVersion> {
83 #[derive(Debug, Deserialize)]
84 struct VersionResponse {
85 available: semver::Version,
86 message: String,
87 url: String,
88 }
89
90 let url = format!("https://version-check.strawlab.org/{product}");
91 let uri: hyper::Uri = match url.parse() {
92 Ok(uri) => uri,
93 Err(e) => {
94 warn!("invalid version-check URL {url}: {e}");
95 return None;
96 }
97 };
98
99 let req = hyper::Request::builder()
100 .uri(&uri)
101 .header(hyper::header::USER_AGENT, user_agent)
102 .body(empty_body())
103 .unwrap();
104
105 let res =
107 match tokio::time::timeout(Duration::from_secs(30), self.client.request(req)).await {
108 Ok(Ok(res)) => res,
109 Ok(Err(e)) => {
110 warn!("version check request to {url} failed: {e}");
111 return None;
112 }
113 Err(_elapsed) => {
114 warn!("version check request to {url} timed out");
115 return None;
116 }
117 };
118
119 if res.status() != hyper::StatusCode::OK {
120 return None;
121 }
122
123 let data = match res.into_body().collect().await {
124 Ok(collected) => collected.to_bytes(),
125 Err(e) => {
126 warn!("could not read version response from {url}: {e}");
127 return None;
128 }
129 };
130
131 match serde_json::from_slice::<VersionResponse>(&data) {
132 Ok(v) => Some(AvailableVersion {
133 version: v.available,
134 message: v.message,
135 url: v.url,
136 }),
137 Err(e) => {
138 warn!("could not parse version response JSON from {url}: {e}");
139 None
140 }
141 }
142 }
143}