geph4_protocol/binder/
client.rs

1use std::{convert::TryInto, time::Duration};
2
3use async_compat::CompatExt;
4use async_trait::async_trait;
5
6use nanorpc::RpcTransport;
7
8use reqwest::{
9    header::{HeaderMap, HeaderName},
10    StatusCode,
11};
12
13use super::protocol::{box_decrypt, box_encrypt};
14
15/// An end-to-end encrypted, HTTP-based RpcTransport implementation. This is used as the main backend for communicating over domain fronting and other systems that hit a particular HTTP endpoint with a particular set of headers.
16pub struct E2eeHttpTransport {
17    binder_lpk: x25519_dalek::PublicKey,
18    endpoint: String,
19    client: reqwest::Client,
20}
21
22#[async_trait]
23impl RpcTransport for E2eeHttpTransport {
24    type Error = anyhow::Error;
25
26    async fn call_raw(
27        &self,
28        req: nanorpc::JrpcRequest,
29    ) -> Result<nanorpc::JrpcResponse, Self::Error> {
30        let eph_sk = x25519_dalek::StaticSecret::new(rand::thread_rng());
31        let encrypted_req =
32            box_encrypt(&serde_json::to_vec(&req)?, eph_sk.clone(), self.binder_lpk);
33        let resp = self
34            .client
35            .post(&self.endpoint)
36            .body(encrypted_req)
37            .send()
38            .compat()
39            .await?;
40        if resp.status() != StatusCode::OK {
41            anyhow::bail!("non-200 status: {}", resp.status());
42        }
43        let encrypted_resp = resp.bytes().compat().await?;
44        let (resp, _) = box_decrypt(&encrypted_resp, eph_sk)?;
45        Ok(serde_json::from_slice(&resp)?)
46    }
47}
48
49impl E2eeHttpTransport {
50    /// Creates a new E2eeHttpTransport instance.
51    pub fn new(binder_lpk: [u8; 32], endpoint: String, headers: Vec<(String, String)>) -> Self {
52        Self {
53            binder_lpk: x25519_dalek::PublicKey::from(binder_lpk),
54            endpoint,
55            client: reqwest::ClientBuilder::new()
56                .default_headers({
57                    let mut hh = HeaderMap::new();
58                    for (k, v) in headers {
59                        hh.insert::<HeaderName>(
60                            k.to_ascii_lowercase().try_into().unwrap(),
61                            v.to_ascii_lowercase().parse().unwrap(),
62                        );
63                    }
64                    hh
65                })
66                .no_proxy()
67                .http1_only()
68                .pool_idle_timeout(Duration::from_secs(1)) // reduce linkability by forcing new connections
69                .build()
70                .unwrap(),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use async_compat::CompatExt;
78    use reqwest::header::HeaderMap;
79
80    #[test]
81    fn reqwest_domain_front() {
82        smolscale::block_on(
83            async move {
84                let client = reqwest::ClientBuilder::new()
85                    .default_headers({
86                        let mut hh = HeaderMap::new();
87                        hh.insert("host", "loving-bell-981479.netlify.app".parse().unwrap());
88                        hh
89                    })
90                    .build()
91                    .unwrap();
92                let resp = client
93                    .get("https://www.netlify.com/v4")
94                    .send()
95                    .await
96                    .unwrap();
97                dbg!(resp);
98            }
99            .compat(),
100        );
101    }
102}