1use std::fs::File;
2use std::path::Path;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::Duration;
6use std::{env, fmt};
7
8use bytes::Buf;
9use chrono::{DateTime, Utc};
10use http_body_util::{BodyExt, Full};
11use hyper::body::Bytes;
12use hyper::Request;
13use hyper_rustls::HttpsConnectorBuilder;
14use hyper_util::client::legacy::Client;
15use hyper_util::rt::TokioExecutor;
16use ring::rand::SystemRandom;
17use ring::signature::{RsaKeyPair, RSA_PKCS1_SHA256};
18use serde::{Deserialize, Deserializer};
19use tracing::{debug, warn};
20
21use crate::Error;
22
23#[derive(Clone, Debug)]
24pub(crate) struct HttpClient {
25 inner: Client<
26 hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
27 Full<Bytes>,
28 >,
29}
30
31impl HttpClient {
32 pub(crate) fn new() -> Result<Self, Error> {
33 #[cfg(feature = "webpki-roots")]
34 let https = HttpsConnectorBuilder::new().with_webpki_roots();
35 #[cfg(not(feature = "webpki-roots"))]
36 let https = HttpsConnectorBuilder::new()
37 .with_native_roots()
38 .map_err(|err| {
39 Error::Io("failed to load native TLS root certificates for HTTPS", err)
40 })?;
41
42 Ok(Self {
43 inner: Client::builder(TokioExecutor::new())
44 .build(https.https_or_http().enable_http2().build()),
45 })
46 }
47
48 pub(crate) async fn token(
49 &self,
50 request: &impl Fn() -> Request<Full<Bytes>>,
51 provider: &'static str,
52 ) -> Result<Arc<Token>, Error> {
53 let mut retries = 0;
54 let body = loop {
55 let err = match self.request(request(), provider).await {
56 Ok(body) => break body,
58 Err(err) => err,
59 };
60
61 warn!(
62 ?err,
63 provider, retries, "failed to refresh token, trying again..."
64 );
65
66 retries += 1;
67 if retries >= RETRY_COUNT {
68 return Err(err);
69 }
70 };
71
72 serde_json::from_slice(&body)
73 .map_err(|err| Error::Json("failed to deserialize token from response", err))
74 }
75
76 pub(crate) async fn request(
77 &self,
78 req: Request<Full<Bytes>>,
79 provider: &'static str,
80 ) -> Result<Bytes, Error> {
81 debug!(url = ?req.uri(), provider, "requesting token");
82 let (parts, body) = self
83 .inner
84 .request(req)
85 .await
86 .map_err(|err| Error::Other("HTTP request failed", Box::new(err)))?
87 .into_parts();
88
89 let mut body = body
90 .collect()
91 .await
92 .map_err(|err| Error::Http("failed to read HTTP response body", err))?
93 .aggregate();
94
95 let body = body.copy_to_bytes(body.remaining());
96 if !parts.status.is_success() {
97 let body = String::from_utf8_lossy(body.as_ref());
98 warn!(%body, status = ?parts.status, "token request failed");
99 return Err(Error::Str("token request failed"));
100 }
101
102 Ok(body)
103 }
104}
105
106#[derive(Clone, Deserialize)]
121pub struct Token {
122 access_token: String,
123 #[serde(
124 deserialize_with = "deserialize_time",
125 rename(deserialize = "expires_in")
126 )]
127 expires_at: DateTime<Utc>,
128}
129
130impl Token {
131 pub(crate) fn from_string(access_token: String, expires_in: Duration) -> Self {
132 Token {
133 access_token,
134 expires_at: Utc::now() + expires_in,
135 }
136 }
137
138 pub fn has_expired(&self) -> bool {
149 self.expires_at - Duration::from_secs(20) <= Utc::now()
150 }
151
152 pub fn as_str(&self) -> &str {
154 &self.access_token
155 }
156
157 pub fn expires_at(&self) -> DateTime<Utc> {
159 self.expires_at
160 }
161}
162
163impl fmt::Debug for Token {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.debug_struct("Token")
166 .field("access_token", &"****")
167 .field("expires_at", &self.expires_at)
168 .finish()
169 }
170}
171
172pub struct Signer {
174 key: RsaKeyPair,
175 rng: SystemRandom,
176}
177
178impl Signer {
179 pub(crate) fn new(pem_pkcs8: &str) -> Result<Self, Error> {
180 let key = match rustls_pemfile::private_key(&mut pem_pkcs8.as_bytes()) {
181 Ok(Some(key)) => key,
182 Ok(None) => {
183 return Err(Error::Str(
184 "no private key found in credentials private key data",
185 ))
186 }
187 Err(err) => {
188 return Err(Error::Io(
189 "failed to read credentials private key data",
190 err,
191 ))
192 }
193 };
194
195 Ok(Signer {
196 key: RsaKeyPair::from_pkcs8(key.secret_der())
197 .map_err(|_| Error::Str("invalid private key in credentials"))?,
198 rng: SystemRandom::new(),
199 })
200 }
201
202 pub fn sign(&self, input: &[u8]) -> Result<Vec<u8>, Error> {
204 let mut signature = vec![0; self.key.public().modulus_len()];
205 self.key
206 .sign(&RSA_PKCS1_SHA256, &self.rng, input, &mut signature)
207 .map_err(|_| Error::Str("failed to sign with credentials key"))?;
208 Ok(signature)
209 }
210}
211
212impl fmt::Debug for Signer {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 f.debug_struct("Signer").finish()
215 }
216}
217
218fn deserialize_time<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
219where
220 D: Deserializer<'de>,
221{
222 let seconds_from_now: u64 = Deserialize::deserialize(deserializer)?;
223 Ok(Utc::now() + Duration::from_secs(seconds_from_now))
224}
225
226#[derive(Deserialize)]
227pub(crate) struct ServiceAccountKey {
228 pub(crate) project_id: Option<Arc<str>>,
230 pub(crate) private_key: String,
232 pub(crate) client_email: String,
234 pub(crate) token_uri: String,
236}
237
238impl ServiceAccountKey {
239 pub(crate) fn from_env() -> Result<Option<Self>, Error> {
240 env::var_os("GOOGLE_APPLICATION_CREDENTIALS")
241 .map(|path| {
242 debug!(
243 ?path,
244 "reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"
245 );
246 Self::from_file(&path)
247 })
248 .transpose()
249 }
250
251 pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
252 let file = File::open(path.as_ref())
253 .map_err(|err| Error::Io("failed to open application credentials file", err))?;
254 serde_json::from_reader(file)
255 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
256 }
257}
258
259impl FromStr for ServiceAccountKey {
260 type Err = Error;
261
262 fn from_str(s: &str) -> Result<Self, Self::Err> {
263 serde_json::from_str(s)
264 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
265 }
266}
267
268impl fmt::Debug for ServiceAccountKey {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 f.debug_struct("ApplicationCredentials")
271 .field("client_email", &self.client_email)
272 .field("project_id", &self.project_id)
273 .finish_non_exhaustive()
274 }
275}
276
277#[derive(Deserialize)]
278pub(crate) struct AuthorizedUserRefreshToken {
279 pub(crate) client_id: String,
281 pub(crate) client_secret: String,
283 pub(crate) quota_project_id: Option<Arc<str>>,
285 pub(crate) refresh_token: String,
287}
288
289impl AuthorizedUserRefreshToken {
290 pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
291 let file = File::open(path.as_ref())
292 .map_err(|err| Error::Io("failed to open application credentials file", err))?;
293 serde_json::from_reader(file)
294 .map_err(|err| Error::Json("failed to deserialize ApplicationCredentials", err))
295 }
296}
297
298impl fmt::Debug for AuthorizedUserRefreshToken {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 f.debug_struct("UserCredentials")
301 .field("client_id", &self.client_id)
302 .field("quota_project_id", &self.quota_project_id)
303 .finish_non_exhaustive()
304 }
305}
306
307const RETRY_COUNT: u8 = 5;
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_deserialize_with_time() {
316 let s = r#"{"access_token":"abc123","expires_in":100}"#;
317 let token: Token = serde_json::from_str(s).unwrap();
318 let expires = Utc::now() + Duration::from_secs(100);
319
320 assert_eq!(token.as_str(), "abc123");
321
322 let expires_at = token.expires_at();
324 assert!(expires_at < expires + Duration::from_secs(1));
325 assert!(expires_at > expires - Duration::from_secs(1));
326 }
327}