Skip to main content

made_client/
made_client.rs

1use std::cmp::min;
2
3use made_proto::v1::made_service_client::MadeServiceClient;
4use prost::Message;
5use sha2::{Digest, Sha256};
6use tokio::time::sleep;
7use tonic::metadata::MetadataValue;
8use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
9use tonic::Request;
10
11use crate::{ClientConfig, MadeClientError, RequestContext};
12
13/// Cloneable reference client backed exclusively by MADE's public gRPC API.
14#[derive(Clone, Debug)]
15pub struct MadeClient {
16    channel: Channel,
17    request_context: Option<RequestContext>,
18}
19
20impl MadeClient {
21    pub async fn connect(endpoint: impl Into<String>) -> Result<Self, MadeClientError> {
22        Self::connect_with_config(ClientConfig::new(endpoint)).await
23    }
24
25    pub async fn connect_with_config(config: ClientConfig) -> Result<Self, MadeClientError> {
26        let mut endpoint = Endpoint::from_shared(config.endpoint().to_owned())
27            .map_err(|error| MadeClientError::InvalidEndpoint(error.to_string()))?;
28        if config.uses_tls() {
29            let mut tls = ClientTlsConfig::new().with_native_roots();
30            if let Some(pem) = config.ca_certificate_pem() {
31                tls = tls.ca_certificate(Certificate::from_pem(pem));
32            }
33            if let Some((certificate, key)) = config.client_identity_pem() {
34                tls = tls.identity(Identity::from_pem(certificate, key));
35            }
36            if let Some(domain_name) = config.tls_domain_name() {
37                tls = tls.domain_name(domain_name);
38            }
39            endpoint = endpoint
40                .tls_config(tls)
41                .map_err(|error| MadeClientError::TlsConfiguration(error.to_string()))?;
42        }
43        let mut delay = config.initial_backoff();
44        let mut last_error = None;
45
46        for attempt in 0..config.connect_attempts() {
47            match endpoint.clone().connect().await {
48                Ok(channel) => {
49                    return Ok(Self {
50                        channel,
51                        request_context: config.request_context().cloned(),
52                    });
53                }
54                Err(error) => last_error = Some(error.to_string()),
55            }
56            if attempt + 1 < config.connect_attempts() {
57                sleep(delay).await;
58                delay = min(delay.saturating_mul(2), config.maximum_backoff());
59            }
60        }
61
62        Err(MadeClientError::ConnectionExhausted(
63            last_error.unwrap_or_else(|| "no connection attempt was made".to_owned()),
64        ))
65    }
66
67    pub(crate) fn rpc(&self) -> MadeServiceClient<Channel> {
68        MadeServiceClient::new(self.channel.clone())
69    }
70
71    pub(crate) fn context(&self) -> RequestContext {
72        self.request_context.clone().unwrap_or_default()
73    }
74
75    pub(crate) fn request<T>(
76        context: &RequestContext,
77        method: &'static str,
78        payload: T,
79    ) -> Request<T>
80    where
81        T: Message,
82    {
83        let payload_bytes = payload.encode_to_vec();
84        let request_id = derived_request_id(context.id(), method, &payload_bytes);
85        let mut request = Request::new(payload);
86        request
87            .metadata_mut()
88            .insert("x-made-request-id", request_id);
89        request
90    }
91
92    #[must_use]
93    pub fn request_context(&self) -> RequestContext {
94        self.context()
95    }
96}
97
98fn derived_request_id(
99    invocation_id: &str,
100    method: &'static str,
101    payload: &[u8],
102) -> MetadataValue<tonic::metadata::Ascii> {
103    let mut digest = Sha256::new();
104    digest.update(b"underpass.made.client-request.v1\0");
105    hash_field(&mut digest, invocation_id.as_bytes());
106    hash_field(&mut digest, method.as_bytes());
107    hash_field(&mut digest, payload);
108    let value = format!("made-{:x}", digest.finalize());
109    MetadataValue::try_from(value).expect("SHA-256 request ids are valid ASCII metadata")
110}
111
112fn hash_field(digest: &mut Sha256, value: &[u8]) {
113    digest.update((value.len() as u64).to_be_bytes());
114    digest.update(value);
115}