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;
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
145#[derive(Clone)]
146pub struct GoogleApiClientBuilderFunction<C>
147where
148 C: Clone + Send,
149{
150 f: fn(GoogleAuthMiddlewareService<Channel>) -> C,
151}
152
153impl<C> GoogleApiClientBuilder<C> for GoogleApiClientBuilderFunction<C>
154where
155 C: Clone + Send,
156{
157 fn create_client(&self, channel: GoogleAuthMiddlewareService<Channel>) -> C {
158 (self.f)(channel)
159 }
160}
161
162impl<C> GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>
163where
164 C: Clone + Send,
165{
166 pub async fn from_function<S: AsRef<str>>(
169 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
170 google_api_url: S,
171 cloud_resource_prefix_meta: Option<String>,
172 ) -> crate::error::Result<Self> {
173 Self::from_function_with_scopes(
174 builder_fn,
175 google_api_url,
176 cloud_resource_prefix_meta,
177 GCP_DEFAULT_SCOPES.clone(),
178 )
179 .await
180 }
181
182 pub async fn from_function_with_headers<S: AsRef<str>>(
183 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
184 google_api_url: S,
185 cloud_resource_prefix_meta: Option<String>,
186 headers: hyper::HeaderMap,
187 ) -> crate::error::Result<Self> {
188 Self::from_function_with_scopes_and_headers(
189 builder_fn,
190 google_api_url,
191 cloud_resource_prefix_meta,
192 GCP_DEFAULT_SCOPES.clone(),
193 headers,
194 )
195 .await
196 }
197
198 pub async fn from_function_with_scopes<S: AsRef<str>>(
206 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
207 google_api_url: S,
208 cloud_resource_prefix_meta: Option<String>,
209 token_scopes: Vec<String>,
210 ) -> crate::error::Result<Self> {
211 Self::from_function_with_scopes_and_headers(
212 builder_fn,
213 google_api_url,
214 cloud_resource_prefix_meta,
215 token_scopes,
216 hyper::HeaderMap::new(),
217 )
218 .await
219 }
220
221 pub async fn from_function_with_scopes_and_headers<S: AsRef<str>>(
222 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
223 google_api_url: S,
224 cloud_resource_prefix_meta: Option<String>,
225 token_scopes: Vec<String>,
226 headers: hyper::HeaderMap,
227 ) -> crate::error::Result<Self> {
228 let channel = GoogleEnvironment::init_google_services_channel(google_api_url).await?;
231 let mut middleware = GoogleAuthMiddlewareLayer::new(
232 GoogleAuthHeaders::from_adc_with_scopes(token_scopes).await?,
233 cloud_resource_prefix_meta,
234 )?;
235 middleware.set_additional_headers(headers);
236 Ok(Self::on_channel(
237 GoogleApiClientBuilderFunction { f: builder_fn },
238 channel,
239 middleware,
240 ))
241 }
242
243 pub async fn from_function_with_credentials<S: AsRef<str>>(
253 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
254 google_api_url: S,
255 cloud_resource_prefix_meta: Option<String>,
256 credentials: Credentials,
257 ) -> crate::error::Result<Self> {
258 Self::from_function_with_credentials_and_headers(
259 builder_fn,
260 google_api_url,
261 cloud_resource_prefix_meta,
262 credentials,
263 hyper::HeaderMap::new(),
264 )
265 .await
266 }
267
268 pub async fn from_function_with_credentials_and_headers<S: AsRef<str>>(
269 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
270 google_api_url: S,
271 cloud_resource_prefix_meta: Option<String>,
272 credentials: Credentials,
273 headers: hyper::HeaderMap,
274 ) -> crate::error::Result<Self> {
275 Self::with_credentials_and_headers(
276 GoogleApiClientBuilderFunction { f: builder_fn },
277 google_api_url,
278 cloud_resource_prefix_meta,
279 credentials,
280 headers,
281 )
282 .await
283 }
284
285 pub async fn from_function_with_middleware<S: AsRef<str>>(
286 builder_fn: fn(GoogleAuthMiddlewareService<Channel>) -> C,
287 google_api_url: S,
288 middleware: GoogleAuthMiddlewareLayer,
289 ) -> crate::error::Result<Self> {
290 Self::with_middleware(
291 GoogleApiClientBuilderFunction { f: builder_fn },
292 google_api_url,
293 middleware,
294 )
295 .await
296 }
297}
298
299pub type GoogleAuthMiddleware = GoogleAuthMiddlewareService<Channel>;
300pub type GoogleApi<C> = GoogleApiClient<GoogleApiClientBuilderFunction<C>, C>;
301
302pub struct GoogleEnvironment;
303
304impl GoogleEnvironment {
305 pub async fn detect_google_project_id() -> Option<String> {
310 Self::detect_project_id_in(
311 |name| std::env::var(name).ok(),
312 AdcFile::load(),
313 MetadataServer::from_env(),
314 )
315 .await
316 }
317
318 async fn detect_project_id_in(
319 env: impl Fn(&str) -> Option<String>,
320 adc: Option<AdcFile>,
321 metadata: MetadataServer,
322 ) -> Option<String> {
323 if let Some(project_id) = PROJECT_ID_ENV_VARS.into_iter().find_map(env) {
324 debug!("Detected GCP Project ID using environment variables");
325 return Some(project_id);
326 }
327 if let Some(project_id) = adc.and_then(|adc| adc.project_id().map(ToString::to_string)) {
328 debug!("Detected GCP Project ID in the Application Default Credentials file");
329 return Some(project_id);
330 }
331 let project_id = metadata.project_id().await;
332 if project_id.is_some() {
333 debug!("Detected GCP Project ID using the metadata server");
334 } else {
335 debug!(
336 env = ?PROJECT_ID_ENV_VARS,
337 "No GCP Project ID detected in this environment; set one of the environment variables"
338 );
339 }
340 project_id
341 }
342
343 pub async fn init_google_services_channel<S: AsRef<str>>(
344 api_url: S,
345 ) -> Result<Channel, crate::error::Error> {
346 let api_url_string = api_url.as_ref().to_string();
347 let base_config = Channel::from_shared(api_url_string.clone())?
348 .connect_timeout(Duration::from_secs(30))
349 .tcp_keepalive(Some(Duration::from_secs(60)))
350 .keep_alive_timeout(Duration::from_secs(60))
351 .http2_keep_alive_interval(Duration::from_secs(60))
352 .keep_alive_while_idle(true);
353
354 let config = if let Some(domain_name) = tls_server_name(&api_url_string)? {
355 let tls_config = Self::init_tls_config(domain_name);
356 base_config.tls_config(tls_config)?
357 } else {
358 base_config
359 };
360
361 Ok(config.connect().await?)
362 }
363
364 #[cfg(not(any(feature = "tls-roots", feature = "tls-webpki-roots")))]
365 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
366 tonic::transport::ClientTlsConfig::new()
367 .ca_certificate(tonic::transport::Certificate::from_pem(
368 crate::apis::CERTIFICATES,
369 ))
370 .domain_name(domain_name)
371 }
372
373 #[cfg(feature = "tls-roots")]
374 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
375 tonic::transport::ClientTlsConfig::new()
376 .with_native_roots()
377 .domain_name(domain_name)
378 }
379
380 #[cfg(all(feature = "tls-webpki-roots", not(feature = "tls-roots")))]
381 fn init_tls_config(domain_name: String) -> tonic::transport::ClientTlsConfig {
382 tonic::transport::ClientTlsConfig::new()
383 .with_webpki_roots()
384 .domain_name(domain_name)
385 }
386}
387
388fn tls_server_name(api_url: &str) -> Result<Option<String>, crate::error::Error> {
394 let uri: hyper::http::Uri = api_url.parse()?;
395 let invalid = |reason: &str| {
396 crate::error::Error::from(crate::error::ErrorKind::InvalidApiUrl(format!(
397 "{reason}: {api_url}"
398 )))
399 };
400 match uri.scheme_str() {
401 Some("https") => {
402 let host = uri
403 .host()
404 .filter(|host| !host.is_empty())
405 .ok_or_else(|| invalid("missing host"))?;
406 let host = host
407 .strip_prefix('[')
408 .and_then(|h| h.strip_suffix(']'))
409 .unwrap_or(host);
410 Ok(Some(host.to_string()))
411 }
412 Some("http") => Ok(None),
413 Some(_) => Err(invalid("unsupported scheme")),
414 None => Err(invalid("missing scheme")),
415 }
416}
417
418const PROJECT_ID_ENV_VARS: [&str; 4] = [
420 "GCP_PROJECT",
421 "PROJECT_ID",
422 "GCP_PROJECT_ID",
423 "GOOGLE_CLOUD_PROJECT",
424];
425
426pub static GCP_DEFAULT_SCOPES: Lazy<Vec<String>> =
427 Lazy::new(|| vec!["https://www.googleapis.com/auth/cloud-platform".into()]);
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::test_support::{StubCredentials, StubResponse, StubServer};
433 use serde_json::json;
434 use std::sync::atomic::{AtomicUsize, Ordering};
435 use std::sync::Arc;
436
437 #[test]
438 fn tls_server_name_is_the_host_of_a_plain_https_url() {
439 assert_eq!(
440 tls_server_name("https://bigquery.googleapis.com").unwrap(),
441 Some("bigquery.googleapis.com".to_string())
442 );
443 }
444
445 #[test]
446 fn tls_server_name_ignores_a_trailing_slash() {
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_the_path() {
455 assert_eq!(
456 tls_server_name("https://bigquery.googleapis.com/bigquery/v2").unwrap(),
457 Some("bigquery.googleapis.com".to_string())
458 );
459 }
460
461 #[test]
462 fn tls_server_name_ignores_an_explicit_port() {
463 assert_eq!(
464 tls_server_name("https://bigquery.googleapis.com:443").unwrap(),
465 Some("bigquery.googleapis.com".to_string())
466 );
467 }
468
469 #[test]
470 fn plain_http_url_uses_no_tls() {
471 assert_eq!(tls_server_name("http://localhost:8080").unwrap(), None);
472 assert_eq!(tls_server_name("http://localhost:8080/").unwrap(), None);
473 }
474
475 #[test]
476 fn url_without_scheme_or_host_is_an_error() {
477 assert!(tls_server_name("bigquery.googleapis.com").is_err());
478 assert!(tls_server_name("/bigquery/v2").is_err());
479 assert!(tls_server_name("https://").is_err());
480 assert!(tls_server_name("not a url").is_err());
481 assert!(tls_server_name("").is_err());
482 }
483 async fn probe(service: &mut GoogleAuthMiddlewareService<Channel>) {
487 let req = hyper::Request::builder()
488 .uri("http://127.0.0.1:1/")
489 .body(tonic::body::Body::empty())
490 .unwrap();
491 let ready = tower::ServiceExt::ready(service).await.unwrap();
492 let _ = tower::Service::call(ready, req).await;
493 }
494
495 #[tokio::test]
496 async fn get_with_shares_the_authenticated_channel() {
497 let credentials = StubCredentials::bearer("counted-token");
498 let middleware = GoogleAuthMiddlewareLayer::new(
499 GoogleAuthHeaders::from(Credentials::from(credentials.clone())),
500 None,
501 )
502 .unwrap();
503 let channel = Channel::from_static("http://127.0.0.1:1").connect_lazy();
504 let service: GoogleAuthMiddlewareService<Channel> =
505 ServiceBuilder::new().layer(middleware).service(channel);
506
507 let client: GoogleApiClient<
508 GoogleApiClientBuilderFunction<GoogleAuthMiddlewareService<Channel>>,
509 GoogleAuthMiddlewareService<Channel>,
510 > = GoogleApiClient {
511 builder: GoogleApiClientBuilderFunction { f: |svc| svc },
512 service,
513 _ph: PhantomData,
514 };
515
516 let mut from_get = client.get();
517 let mut from_get_with = client.get_with(|svc| svc);
518
519 probe(&mut from_get).await;
520 probe(&mut from_get_with).await;
521
522 assert_eq!(credentials.served(), 1);
526 }
527
528 async fn silent_endpoint() -> (String, Arc<AtomicUsize>) {
532 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
533 let url = format!("http://{}", listener.local_addr().unwrap());
534 let connections = Arc::new(AtomicUsize::new(0));
535 let counter = connections.clone();
536 tokio::spawn(async move {
537 let mut held = Vec::new();
538 while let Ok((socket, _)) = listener.accept().await {
539 counter.fetch_add(1, Ordering::SeqCst);
540 held.push(socket);
541 }
542 });
543 (url, connections)
544 }
545
546 async fn send_stalled(mut service: GoogleAuthMiddlewareService<Channel>) {
547 let _ = tokio::time::timeout(Duration::from_millis(200), probe(&mut service)).await;
548 }
549
550 #[tokio::test]
551 async fn connect_with_endpoint_shares_auth_with_a_client_on_another_host() {
552 let credentials = StubCredentials::bearer("counted-token");
553 let (first_url, first_connections) = silent_endpoint().await;
554 let (second_url, second_connections) = silent_endpoint().await;
555
556 let first: GoogleApi<GoogleAuthMiddlewareService<Channel>> =
557 GoogleApi::from_function_with_credentials(
558 |svc| svc,
559 first_url,
560 None,
561 Credentials::from(credentials.clone()),
562 )
563 .await
564 .unwrap();
565 let second = first
566 .connect_with_endpoint(|svc| svc, second_url, None)
567 .await
568 .unwrap();
569
570 send_stalled(first.get()).await;
571 send_stalled(second.get()).await;
572
573 assert_eq!(first_connections.load(Ordering::SeqCst), 1);
574 assert_eq!(second_connections.load(Ordering::SeqCst), 1);
575 assert_eq!(credentials.served(), 1);
576 }
577
578 async fn detected_project_id(
579 env: &[(&str, &str)],
580 adc: Option<serde_json::Value>,
581 metadata: &StubServer,
582 ) -> Option<String> {
583 GoogleEnvironment::detect_project_id_in(
584 |name| {
585 env.iter()
586 .find(|(set, _)| *set == name)
587 .map(|(_, value)| value.to_string())
588 },
589 adc.map(AdcFile::from),
590 MetadataServer::at(metadata.url.clone()),
591 )
592 .await
593 }
594
595 #[tokio::test]
596 async fn project_id_sources_are_read_in_order() {
597 let metadata = StubServer::start(vec![StubResponse::text("200 OK", "metadata-project")
598 .with_header("Metadata-Flavor", "Google")])
599 .await;
600 let mut env = vec![
601 ("GCP_PROJECT", "gcp-project"),
602 ("PROJECT_ID", "project-id"),
603 ("GCP_PROJECT_ID", "gcp-project-id"),
604 ("GOOGLE_CLOUD_PROJECT", "google-cloud-project"),
605 ];
606 let mut adc = json!({
607 "type": "impersonated_service_account",
608 "project_id": "adc-project",
609 "quota_project_id": "adc-quota-project",
610 "source_credentials": {
611 "type": "service_account",
612 "project_id": "source-project",
613 "quota_project_id": "source-quota-project",
614 },
615 });
616
617 let mut detected = Vec::new();
618 while !env.is_empty() {
619 detected.push(detected_project_id(&env, Some(adc.clone()), &metadata).await);
620 env.remove(0);
621 }
622 for field in [
623 "/project_id",
624 "/quota_project_id",
625 "/source_credentials/project_id",
626 ] {
627 detected.push(detected_project_id(&env, Some(adc.clone()), &metadata).await);
628 let (parent, name) = field.rsplit_once('/').unwrap();
629 adc.pointer_mut(parent)
630 .and_then(serde_json::Value::as_object_mut)
631 .unwrap()
632 .remove(name);
633 }
634 detected.push(detected_project_id(&env, Some(adc), &metadata).await);
635 detected.push(detected_project_id(&env, None, &metadata).await);
636
637 assert_eq!(
638 detected,
639 [
640 "gcp-project",
641 "project-id",
642 "gcp-project-id",
643 "google-cloud-project",
644 "adc-project",
645 "adc-quota-project",
646 "source-project",
647 "source-quota-project",
648 "metadata-project",
649 ]
650 .map(|project| Some(project.to_string()))
651 );
652 }
653}