1use std::marker::PhantomData;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use google_cloud_auth::credentials::Credentials;
6use once_cell::sync::Lazy;
7use tonic::transport::Channel;
8use tower::ServiceBuilder;
9use tracing::*;
10
11use crate::adc::AdcFile;
12use crate::metadata::MetadataServer;
13use crate::middleware::{GoogleAuthMiddlewareLayer, GoogleAuthMiddlewareService};
14use crate::{GoogleAuthHeaders, RequestTimeouts};
15
16#[async_trait]
17pub trait GoogleApiClientBuilder<C>
18where
19 C: Clone + Send,
20{
21 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C;
22}
23
24#[derive(Clone)]
25pub struct GoogleApiClient<B, C>
26where
27 B: GoogleApiClientBuilder<C>,
28 C: Clone + Send,
29{
30 builder: B,
31 service: GoogleAuthMiddlewareService<Channel>,
32 _ph: PhantomData<C>,
33}
34
35impl<B, C> GoogleApiClient<B, C>
36where
37 B: GoogleApiClientBuilder<C>,
38 C: Clone + Send,
39{
40 pub async fn with_credentials<S: AsRef<str>>(
41 builder: B,
42 google_api_url: S,
43 cloud_resource_prefix: Option<String>,
44 credentials: Credentials,
45 ) -> crate::error::Result<Self> {
46 Self::with_credentials_and_headers(
47 builder,
48 google_api_url,
49 cloud_resource_prefix,
50 credentials,
51 hyper::HeaderMap::new(),
52 )
53 .await
54 }
55
56 pub async fn with_credentials_and_headers<S: AsRef<str>>(
57 builder: B,
58 google_api_url: S,
59 cloud_resource_prefix: Option<String>,
60 credentials: Credentials,
61 additional_headers: hyper::HeaderMap,
62 ) -> crate::error::Result<Self> {
63 debug!(
64 "Creating a new Google API client for {}",
65 google_api_url.as_ref()
66 );
67
68 let mut middleware = GoogleAuthMiddlewareLayer::new(
69 GoogleAuthHeaders::from(credentials),
70 cloud_resource_prefix,
71 )?;
72 middleware.set_additional_headers(additional_headers);
73
74 Self::with_middleware(builder, google_api_url, middleware).await
75 }
76
77 pub async fn with_middleware<S: AsRef<str>>(
78 builder: B,
79 google_api_url: S,
80 middleware: GoogleAuthMiddlewareLayer,
81 ) -> crate::error::Result<Self> {
82 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
83 Ok(Self::on_channel(builder, channel, middleware))
84 }
85
86 fn on_channel(builder: B, channel: Channel, middleware: GoogleAuthMiddlewareLayer) -> Self {
87 Self {
88 builder,
89 service: ServiceBuilder::new().layer(middleware).service(channel),
90 _ph: PhantomData,
91 }
92 }
93
94 pub fn get(&self) -> C {
95 self.builder.create_client(self.service.clone())
96 }
97
98 pub fn get_with<C2>(&self, f: impl FnOnce(GoogleAuthMiddlewareService<Channel>) -> C2) -> C2 {
102 f(self.service.clone())
103 }
104
105 pub async fn connect_with_endpoint<C2, S: AsRef<str>>(
115 &self,
116 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C2,
117 google_api_url: S,
118 cloud_resource_prefix: Option<String>,
119 ) -> crate::error::Result<GoogleApi<C2>>
120 where
121 C2: Clone + Send,
122 {
123 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
124 Ok(GoogleApiClient {
125 builder: GoogleApiClientBuilderFunction { f: builder_fn },
126 service: self.service.with_inner(channel, cloud_resource_prefix)?,
127 _ph: PhantomData,
128 })
129 }
130
131 pub fn amend_user_agent(mut self, user_agent: String) -> crate::error::Result<Self> {
132 self.service.append_user_agent(user_agent)?;
133 Ok(self)
134 }
135
136 pub fn amend_x_goog_api_client(
137 mut self,
138 x_goog_api_client: String,
139 ) -> crate::error::Result<Self> {
140 self.service.append_x_goog_api_client(x_goog_api_client)?;
141 Ok(self)
142 }
143
144 pub fn with_request_timeouts(mut self, request_timeouts: RequestTimeouts) -> Self {
149 self.service.set_request_timeouts(request_timeouts);
150 self
151 }
152}
153
154#[derive(Clone)]
155pub struct GoogleApiClientBuilderFunction<C>
156where
157 C: Clone + Send,
158{
159 f: fn(GoogleAuthMiddlewareService<Channel>) -> C,
160}
161
162impl<C> GoogleApiClientBuilder<C> for GoogleApiClientBuilderFunction<C>
163where
164 C: Clone + Send,
165{
166 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C {
167 (self.f)(channel)
168 }
169}
170
171impl<C> GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>
172where
173 C: Clone + Send,
174{
175 pub async fn from_function<S: AsRef<str>>(
178 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
179 google_api_url: S,
180 cloud_resource_prefix_meta: Option<String>,
181 ) -> crate::error::Result<Self> {
182 Self::from_function_with_scopes(
183 builder_fn,
184 google_api_url,
185 cloud_resource_prefix_meta,
186 GCP_DEFAULT_SCOPES.clone(),
187 )
188 .await
189 }
190
191 pub async fn from_function_with_headers<S: AsRef<str>>(
192 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
193 google_api_url: S,
194 cloud_resource_prefix_meta: Option<String>,
195 headers: hyper::HeaderMap,
196 ) -> crate::error::Result<Self> {
197 Self::from_function_with_scopes_and_headers(
198 builder_fn,
199 google_api_url,
200 cloud_resource_prefix_meta,
201 GCP_DEFAULT_SCOPES.clone(),
202 headers,
203 )
204 .await
205 }
206
207 pub async fn from_function_with_scopes<S: AsRef<str>>(
215 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
216 google_api_url: S,
217 cloud_resource_prefix_meta: Option<String>,
218 token_scopes: Vec<String>,
219 ) -> crate::error::Result<Self> {
220 Self::from_function_with_scopes_and_headers(
221 builder_fn,
222 google_api_url,
223 cloud_resource_prefix_meta,
224 token_scopes,
225 hyper::HeaderMap::new(),
226 )
227 .await
228 }
229
230 pub async fn from_function_with_scopes_and_headers<S: AsRef<str>>(
231 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
232 google_api_url: S,
233 cloud_resource_prefix_meta: Option<String>,
234 token_scopes: Vec<String>,
235 headers: hyper::HeaderMap,
236 ) -> crate::error::Result<Self> {
237 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
240 let mut middleware = GoogleAuthMiddlewareLayer::new(
241 GoogleAuthHeaders::from_adc_with_scopes(token_scopes).await?,
242 cloud_resource_prefix_meta,
243 )?;
244 middleware.set_additional_headers(headers);
245 Ok(Self::on_channel(
246 GoogleApiClientBuilderFunction { f: builder_fn },
247 channel,
248 middleware,
249 ))
250 }
251
252 pub async fn from_function_with_credentials<S: AsRef<str>>(
262 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
263 google_api_url: S,
264 cloud_resource_prefix_meta: Option<String>,
265 credentials: Credentials,
266 ) -> crate::error::Result<Self> {
267 Self::from_function_with_credentials_and_headers(
268 builder_fn,
269 google_api_url,
270 cloud_resource_prefix_meta,
271 credentials,
272 hyper::HeaderMap::new(),
273 )
274 .await
275 }
276
277 pub async fn from_function_with_credentials_and_headers<S: AsRef<str>>(
278 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
279 google_api_url: S,
280 cloud_resource_prefix_meta: Option<String>,
281 credentials: Credentials,
282 headers: hyper::HeaderMap,
283 ) -> crate::error::Result<Self> {
284 Self::with_credentials_and_headers(
285 GoogleApiClientBuilderFunction { f: builder_fn },
286 google_api_url,
287 cloud_resource_prefix_meta,
288 credentials,
289 headers,
290 )
291 .await
292 }
293
294 pub async fn from_function_with_middleware<S: AsRef<str>>(
295 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
296 google_api_url: S,
297 middleware: GoogleAuthMiddlewareLayer,
298 ) -> crate::error::Result<Self> {
299 Self::with_middleware(
300 GoogleApiClientBuilderFunction { f: builder_fn },
301 google_api_url,
302 middleware,
303 )
304 .await
305 }
306}
307
308pub type GoogleAuthMiddleware = GoogleAuthMiddlewareService<Channel>;
309pub type GoogleApi<C> = GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>;
310
311pub struct GoogleEnvironment;
312
313impl GoogleEnvironment {
314 pub async fn detect_google_project_id() -> Option<String> {
319 Self::detect_project_id_in(
320 |name| std::env::var(name).ok(),
321 AdcFile::load(),
322 MetadataServer::from_env(),
323 )
324 .await
325 }
326
327 async fn detect_project_id_in(
328 env: impl Fn(&str) -> Option<String>,
329 adc: Option<AdcFile>,
330 metadata: MetadataServer,
331 ) -> Option<String> {
332 if let Some(project_id) = PROJECT_ID_ENV_VARS.into_iter().find_map(env) {
333 debug!("Detected GCP Project ID using environment variables");
334 return Some(project_id);
335 }
336 if let Some(project_id) = adc.and_then(|adc| adc.project_id().map(ToString::to_string)) {
337 debug!("Detected GCP Project ID in the Application Default Credentials file");
338 return Some(project_id);
339 }
340 let project_id = metadata.project_id().await;
341 if project_id.is_some() {
342 debug!("Detected GCP Project ID using the metadata server");
343 } else {
344 debug!(
345 env = ?PROJECT_ID_ENV_VARS,
346 "No GCP Project ID detected in this environment; set one of the environment variables"
347 );
348 }
349 project_id
350 }
351
352 pub async fn init_google_services_channel<S: AsRef<str>>(
353 api_url: S,
354 ) -> Result<Channel, crate::error::Error> {
355 let api_url_string = api_url.as_ref().to_string();
356 let base_config = Channel::from_shared(api_url_string.clone())?
357 .connect_timeout(Duration::from_secs(30))
358 .tcp_keepalive(Some(Duration::from_secs(60)))
359 .keep_alive_timeout(Duration::from_secs(60))
360 .http2_keep_alive_interval(Duration::from_secs(60))
361 .keep_alive_while_idle(true);
362
363 let config = if let Some(domain_name) = tls_server_name(&api_url_string)? {
364 let tls_config = Self::init_tls_config(domain_name);
365 base_config.tls_config(tls_config)?
366 } else {
367 base_config
368 };
369
370 Ok(config.connect().await?)
371 }
372
373 #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
374 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
375 tonic::transport::ClientTlsConfig::new()
376 .ca_certificate(tonic::transport::Certificate::from_pem(
377 crate::apis::CERTIFICATES,
378 ))
379 .domain_name(domain_name)
380 }
381
382 #[cfg(feature = "tls-roots")]
383 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
384 tonic::transport::ClientTlsConfig::new()
385 .with_native_roots()
386 .domain_name(domain_name)
387 }
388
389 #[cfg(all(feature = "tls-webpki-roots", not(feature = "tls-roots")))]
390 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
391 tonic::transport::ClientTlsConfig::new()
392 .with_webpki_roots()
393 .domain_name(domain_name)
394 }
395}
396
397fn tls_server_name(api_url: &str) -> Result<Option<String>, crate::error::Error> {
403 let uri: hyper::http::Uri = api_url.parse()?;
404 let invalid = |reason: &str| {
405 crate::error::Error::from(crate::error::ErrorKind::InvalidApiUrl(format!(
406 "{reason}: {api_url}"
407 )))
408 };
409 match uri.scheme_str() {
410 Some("https") => {
411 let host = uri
412 .host()
413 .filter(|host| !host.is_empty())
414 .ok_or_else(|| invalid("missing host"))?;
415 let host = host
416 .strip_prefix('[')
417 .and_then(|h| h.strip_suffix(']'))
418 .unwrap_or(host);
419 Ok(Some(host.to_string()))
420 }
421 Some("http") => Ok(None),
422 Some(_) => Err(invalid("unsupported scheme")),
423 None => Err(invalid("missing scheme")),
424 }
425}
426
427const PROJECT_ID_ENV_VARS: [&str; 4] = [
429 "GCP_PROJECT",
430 "PROJECT_ID",
431 "GCP_PROJECT_ID",
432 "GOOGLE_CLOUD_PROJECT",
433];
434
435pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
436 Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::test_support::{silent_endpoint, StubCredentials, StubResponse, StubServer};
442 use serde_json::json;
443 use std::sync::atomic::Ordering;
444
445 #[test]
446 fn tls_server_name_is_the_host_of_a_plain_https_url() {
447 assert_eq!(
448 tls_server_name("https://bigquery.googleapis.com").unwrap(),
449 Some("bigquery.googleapis.com".to_string())
450 );
451 }
452
453 #[test]
454 fn tls_server_name_ignores_a_trailing_slash() {
455 assert_eq!(
456 tls_server_name("https://bigquery.googleapis.com/").unwrap(),
457 Some("bigquery.googleapis.com".to_string())
458 );
459 }
460
461 #[test]
462 fn tls_server_name_ignores_the_path() {
463 assert_eq!(
464 tls_server_name("https://bigquery.googleapis.com/bigquery/v2").unwrap(),
465 Some("bigquery.googleapis.com".to_string())
466 );
467 }
468
469 #[test]
470 fn tls_server_name_ignores_an_explicit_port() {
471 assert_eq!(
472 tls_server_name("https://bigquery.googleapis.com:443").unwrap(),
473 Some("bigquery.googleapis.com".to_string())
474 );
475 }
476
477 #[test]
478 fn plain_http_url_uses_no_tls() {
479 assert_eq!(tls_server_name("http://localhost:8080").unwrap(), None);
480 assert_eq!(tls_server_name("http://localhost:8080/").unwrap(), None);
481 }
482
483 #[test]
484 fn url_without_scheme_or_host_is_an_error() {
485 assert!(tls_server_name("bigquery.googleapis.com").is_err());
486 assert!(tls_server_name("/bigquery/v2").is_err());
487 assert!(tls_server_name("https://").is_err());
488 assert!(tls_server_name("not a url").is_err());
489 assert!(tls_server_name("").is_err());
490 }
491 async fn probe(service: &mut GoogleAuthMiddlewareService<Channel>) {
495 let req = hyper::Request::builder()
496 .uri("http://127.0.0.1:1/")
497 .body(tonic::body::Body::empty())
498 .unwrap();
499 let ready = tower::ServiceExt::ready(service).await.unwrap();
500 let _ = tower::Service::call(ready, req).await;
501 }
502
503 #[tokio::test]
504 async fn get_with_shares_the_authenticated_channel() {
505 let credentials = StubCredentials::bearer("counted-token");
506 let middleware = GoogleAuthMiddlewareLayer::new(
507 GoogleAuthHeaders::from(Credentials::from(credentials.clone())),
508 None,
509 )
510 .unwrap();
511 let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
512 let service: GoogleAuthMiddlewareService<Channel> =
513 ServiceBuilder::new().layer(middleware).service(channel);
514
515 let client: GoogleApiClient<
516 GoogleApiClientBuilderFunction<GoogleAuthMiddlewareService<Channel>>,
517 GoogleAuthMiddlewareService<Channel>,
518 > = GoogleApiClient {
519 builder: GoogleApiClientBuilderFunction { f: |svc| svc },
520 service,
521 _ph: PhantomData,
522 };
523
524 let mut from_get = client.get();
525 let mut from_get_with = client.get_with(|svc| svc);
526
527 probe(&mut from_get).await;
528 probe(&mut from_get_with).await;
529
530 assert_eq!(credentials.served(), 1);
534 }
535
536 async fn send_stalled(mut service: GoogleAuthMiddlewareService<Channel>) {
537 let _ = tokio::time::timeout(Duration::from_millis(200), probe(&mut service)).await;
538 }
539
540 #[tokio::test]
541 async fn connect_with_endpoint_shares_auth_with_a_client_on_another_host() {
542 let credentials = StubCredentials::bearer("counted-token");
543 let (first_url, first_connections) = silent_endpoint().await;
544 let (second_url, second_connections) = silent_endpoint().await;
545
546 let first: GoogleApi<GoogleAuthMiddlewareService<Channel>> =
547 GoogleApi::from_function_with_credentials(
548 |svc| svc,
549 first_url,
550 None,
551 Credentials::from(credentials.clone()),
552 )
553 .await
554 .unwrap();
555 let second = first
556 .connect_with_endpoint(|svc| svc, second_url, None)
557 .await
558 .unwrap();
559
560 send_stalled(first.get()).await;
561 send_stalled(second.get()).await;
562
563 assert_eq!(first_connections.load(Ordering::SeqCst), 1);
564 assert_eq!(second_connections.load(Ordering::SeqCst), 1);
565 assert_eq!(credentials.served(), 1);
566 }
567
568 async fn detected_project_id(
569 env: &[(&str, &str)],
570 adc: Option<serde_json::Value>,
571 metadata: &StubServer,
572 ) -> Option<String> {
573 GoogleEnvironment::detect_project_id_in(
574 |name| {
575 env.iter()
576 .find(|(set, _)| *set == name)
577 .map(|(_, value)| value.to_string())
578 },
579 adc.map(AdcFile::from),
580 MetadataServer::at(metadata.url.clone()),
581 )
582 .await
583 }
584
585 #[tokio::test]
586 async fn project_id_sources_are_read_in_order() {
587 let metadata = StubServer::start(vec![StubResponse::text("200 OK", "metadata-project")
588 .with_header("Metadata-Flavor", "Google")])
589 .await;
590 let mut env = vec![
591 ("GCP_PROJECT", "gcp-project"),
592 ("PROJECT_ID", "project-id"),
593 ("GCP_PROJECT_ID", "gcp-project-id"),
594 ("GOOGLE_CLOUD_PROJECT", "google-cloud-project"),
595 ];
596 let mut adc = json!({
597 "type": "impersonated_service_account",
598 "project_id": "adc-project",
599 "quota_project_id": "adc-quota-project",
600 "source_credentials": {
601 "type": "service_account",
602 "project_id": "source-project",
603 "quota_project_id": "source-quota-project",
604 },
605 });
606
607 let mut detected = Vec::new();
608 while !env.is_empty() {
609 detected.push(detected_project_id(&env, Some(adc.clone()), &metadata).await);
610 env.remove(0);
611 }
612 for field in [
613 "/project_id",
614 "/quota_project_id",
615 "/source_credentials/project_id",
616 ] {
617 detected.push(detected_project_id(&env, Some(adc.clone()), &metadata).await);
618 let (parent, name) = field.rsplit_once('/').unwrap();
619 adc.pointer_mut(parent)
620 .and_then(serde_json::Value::as_object_mut)
621 .unwrap()
622 .remove(name);
623 }
624 detected.push(detected_project_id(&env, Some(adc), &metadata).await);
625 detected.push(detected_project_id(&env, None, &metadata).await);
626
627 assert_eq!(
628 detected,
629 [
630 "gcp-project",
631 "project-id",
632 "gcp-project-id",
633 "google-cloud-project",
634 "adc-project",
635 "adc-quota-project",
636 "source-project",
637 "source-quota-project",
638 "metadata-project",
639 ]
640 .map(|project| Some(project.to_string()))
641 );
642 }
643}