1#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
2use std::env;
3use std::fmt;
4use std::fs::File;
5use std::path::Path;
6#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Duration;
10
11use bytes::Buf;
12use chrono::{DateTime, Utc};
13use http_body_util::{BodyExt, Full};
14use hyper::body::Bytes;
15use hyper::Request;
16use hyper_rustls::HttpsConnectorBuilder;
17use hyper_util::client::legacy::Client;
18use hyper_util::rt::TokioExecutor;
19use rustls::crypto::CryptoProvider;
20use serde::{Deserialize, Deserializer};
21use tokio::time::sleep;
22use tracing::{debug, warn};
23
24use crate::Error;
25
26#[derive(Clone, Debug)]
27pub(crate) struct HttpClient {
28 inner: Client<
29 hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
30 Full<Bytes>,
31 >,
32}
33
34impl HttpClient {
35 pub(crate) fn new() -> Result<Self, Error> {
36 #[cfg(feature = "webpki-roots")]
37 let https = HttpsConnectorBuilder::new()
38 .with_provider_and_webpki_roots(default_provider()?)
39 .map_err(|err| Error::Other("failed to initialize TLS configuration", Box::new(err)))?;
40 #[cfg(not(feature = "webpki-roots"))]
41 let https = HttpsConnectorBuilder::new()
42 .with_provider_and_native_roots(default_provider()?)
43 .map_err(|err| {
44 Error::Io("failed to load native TLS root certificates for HTTPS", err)
45 })?;
46
47 Ok(Self {
48 inner: Client::builder(TokioExecutor::new())
49 .build(https.https_or_http().enable_http2().build()),
50 })
51 }
52
53 pub(crate) async fn token(
54 &self,
55 request: &impl Fn() -> Request<Full<Bytes>>,
56 provider: &'static str,
57 ) -> Result<Arc<Token>, Error> {
58 let mut sleep_interval = Duration::from_millis(50);
61 let mut retries = 0;
62
63 let body = loop {
64 let err = match self.request(request(), provider).await {
65 Ok(body) => break body,
67 Err(err) => err,
68 };
69
70 warn!(
71 ?err,
72 provider, retries, "failed to refresh token, trying again..."
73 );
74
75 retries += 1;
76 if retries >= RETRY_COUNT {
77 return Err(err);
78 }
79
80 sleep(sleep_interval).await;
81 sleep_interval *= 2;
82 };
83
84 serde_json::from_slice(&body)
85 .map_err(|err| Error::Json("failed to deserialize token from response", err))
86 }
87
88 pub(crate) async fn request(
89 &self,
90 req: Request<Full<Bytes>>,
91 provider: &'static str,
92 ) -> Result<Bytes, Error> {
93 debug!(url = ?req.uri(), provider, "requesting token");
94 let (parts, body) = self
95 .inner
96 .request(req)
97 .await
98 .map_err(|err| Error::Other("HTTP request failed", Box::new(err)))?
99 .into_parts();
100
101 let mut body = body
102 .collect()
103 .await
104 .map_err(|err| Error::Http("failed to read HTTP response body", err))?
105 .aggregate();
106
107 let body = body.copy_to_bytes(body.remaining());
108 if !parts.status.is_success() {
109 let body = String::from_utf8_lossy(body.as_ref());
110 warn!(%body, status = ?parts.status, "token request failed");
111 return Err(Error::Str("token request failed"));
112 }
113
114 Ok(body)
115 }
116}
117
118#[cfg(feature = "ring")]
120fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
121 Ok(Arc::new(rustls::crypto::ring::default_provider()))
122}
123
124#[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
126fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
127 Ok(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
128}
129
130#[cfg(not(any(feature = "ring", feature = "aws-lc-rs")))]
135fn default_provider() -> Result<Arc<CryptoProvider>, Error> {
136 CryptoProvider::get_default().cloned().ok_or(Error::Str(
137 "no per-process default crypto provider installed: enable the `ring` or `aws-lc-rs` \
138 feature, or install one with `CryptoProvider::install_default()`",
139 ))
140}
141
142#[derive(Clone, Deserialize)]
157pub struct Token {
158 access_token: String,
159 #[serde(
160 deserialize_with = "deserialize_time",
161 rename(deserialize = "expires_in")
162 )]
163 expires_at: DateTime<Utc>,
164}
165
166impl Token {
167 pub(crate) fn from_string(access_token: String, expires_in: Duration) -> Self {
168 Token {
169 access_token,
170 expires_at: Utc::now() + expires_in,
171 }
172 }
173
174 pub fn has_expired(&self) -> bool {
185 self.expires_at - Duration::from_secs(20) <= Utc::now()
186 }
187
188 pub fn as_str(&self) -> &str {
190 &self.access_token
191 }
192
193 pub fn expires_at(&self) -> DateTime<Utc> {
195 self.expires_at
196 }
197}
198
199impl fmt::Debug for Token {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 f.debug_struct("Token")
202 .field("access_token", &"****")
203 .field("expires_at", &self.expires_at)
204 .finish()
205 }
206}
207
208#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
209pub use self::sign::Signer;
210
211#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
212mod sign {
213 use std::fmt;
214
215 #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
216 use aws_lc_rs::rand::SystemRandom;
217 #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
218 use aws_lc_rs::signature::{KeyPair, RsaKeyPair, RSA_PKCS1_SHA256};
219 #[cfg(feature = "ring")]
220 use ring::rand::SystemRandom;
221 #[cfg(feature = "ring")]
222 use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
223 use rustls_pki_types::pem::PemObject;
224 use rustls_pki_types::PrivatePkcs8KeyDer;
225
226 use crate::Error;
227
228 pub struct Signer {
233 key: RsaKeyPair,
234 rng: SystemRandom,
235 }
236
237 impl Signer {
238 pub(crate) fn new(pem_pkcs8: &str) -> Result<Self, Error> {
239 let key = match PrivatePkcs8KeyDer::from_pem_slice(pem_pkcs8.as_bytes()) {
240 Ok(key) => key,
241 Err(err) => {
242 return Err(Error::Other(
243 "failed to parse PKCS#8 RSA key pair",
244 err.into(),
245 ))
246 }
247 };
248
249 Ok(Signer {
250 key: RsaKeyPair::from_pkcs8(key.secret_pkcs8_der())
251 .map_err(|_| Error::Str("invalid private key in credentials"))?,
252 rng: SystemRandom::new(),
253 })
254 }
255
256 pub fn sign(&self, input: &[u8]) -> Result<Vec<u8>, Error> {
258 #[cfg(feature = "ring")]
259 let modulus_len = self.key.public().modulus_len();
260 #[cfg(all(not(feature = "ring"), feature = "aws-lc-rs"))]
261 let modulus_len = self.key.public_key().modulus_len();
262
263 let mut signature = vec![0; modulus_len];
264 self.key
265 .sign(&RSA_PKCS1_SHA256, &self.rng, input, &mut signature)
266 .map_err(|_| Error::Str("failed to sign with credentials key"))?;
267 Ok(signature)
268 }
269 }
270
271 impl fmt::Debug for Signer {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.debug_struct("Signer").finish()
274 }
275 }
276}
277
278fn deserialize_time<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
279where
280 D: Deserializer<'de>,
281{
282 let seconds_from_now: u64 = Deserialize::deserialize(deserializer)?;
283 Ok(Utc::now() + Duration::from_secs(seconds_from_now))
284}
285
286#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
287#[derive(Deserialize)]
288pub(crate) struct ServiceAccountKey {
289 pub(crate) project_id: Option<Arc<str>>,
291 pub(crate) private_key: String,
293 pub(crate) client_email: String,
295 pub(crate) token_uri: String,
297}
298
299#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
300impl ServiceAccountKey {
301 pub(crate) fn from_env() -> Result<Option<Self>, Error> {
302 env::var_os("GOOGLE_APPLICATION_CREDENTIALS")
303 .map(|path| {
304 debug!(
305 ?path,
306 "reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"
307 );
308 Self::from_file(&path)
309 })
310 .transpose()
311 }
312
313 pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
314 let file = File::open(path.as_ref())
315 .map_err(|err| Error::Io("failed to open application credentials file", err))?;
316 serde_json::from_reader(file)
317 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
318 }
319}
320
321#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
322impl FromStr for ServiceAccountKey {
323 type Err = Error;
324
325 fn from_str(s: &str) -> Result<Self, Self::Err> {
326 serde_json::from_str(s)
327 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
328 }
329}
330
331#[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
332impl fmt::Debug for ServiceAccountKey {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("ApplicationCredentials")
335 .field("client_email", &self.client_email)
336 .field("project_id", &self.project_id)
337 .finish_non_exhaustive()
338 }
339}
340
341#[derive(Deserialize)]
342pub(crate) struct AuthorizedUserRefreshToken {
343 pub(crate) client_id: String,
345 pub(crate) client_secret: String,
347 pub(crate) quota_project_id: Option<Arc<str>>,
349 pub(crate) refresh_token: String,
351}
352
353impl AuthorizedUserRefreshToken {
354 pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
355 let file = File::open(path.as_ref())
356 .map_err(|err| Error::Io("failed to open application credentials file", err))?;
357 serde_json::from_reader(file)
358 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
359 }
360}
361
362impl fmt::Debug for AuthorizedUserRefreshToken {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 f.debug_struct("UserCredentials")
365 .field("client_id", &self.client_id)
366 .field("quota_project_id", &self.quota_project_id)
367 .finish_non_exhaustive()
368 }
369}
370
371const RETRY_COUNT: u8 = 5;
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_deserialize_with_time() {
380 let s = r#"{"access_token":"abc123","expires_in":100}"#;
381 let token: Token = serde_json::from_str(s).unwrap();
382 let expires = Utc::now() + Duration::from_secs(100);
383
384 assert_eq!(token.as_str(), "abc123");
385
386 let expires_at = token.expires_at();
388 assert!(expires_at < expires + Duration::from_secs(1));
389 assert!(expires_at > expires - Duration::from_secs(1));
390 }
391}