Skip to main content

actr_hyper/
ais_client.rs

1//! AIS HTTP client
2//!
3//! Encapsulates the logic for sending protobuf requests to the AIS `/register` endpoint.
4//! Supports two registration modes:
5//! - Initial registration: authenticate with manifest_raw + mfr_signature
6//! - PSK renewal: renew directly using an existing PSK token
7//! - Linked registration: authenticate with realm authorization
8
9use prost::Message;
10use tracing::{debug, error, info, warn};
11
12use actr_protocol::{RegisterRequest, RegisterResponse};
13
14use crate::error::{HyperError, HyperResult};
15
16/// AIS HTTP client
17///
18/// Encapsulates the logic for sending protobuf requests to the AIS /register endpoint.
19/// All requests use `application/x-protobuf` encoding.
20pub struct AisClient {
21    endpoint: String,
22    http: reqwest::Client,
23    /// Optional realm secret for `x-actrix-realm-secret` header authentication
24    realm_secret: Option<String>,
25}
26
27impl AisClient {
28    /// Create a new AIS client
29    ///
30    /// `endpoint` is the AIS base URL, e.g. `"http://ais.example.com:8080"`.
31    pub fn new(endpoint: impl Into<String>) -> Self {
32        let http = reqwest::Client::builder()
33            .timeout(std::time::Duration::from_secs(30))
34            .build()
35            .expect("reqwest::Client build failed (should never happen)");
36        Self {
37            endpoint: endpoint.into(),
38            http,
39            realm_secret: None,
40        }
41    }
42
43    /// Set the realm secret for authentication
44    pub fn with_realm_secret(mut self, secret: impl Into<String>) -> Self {
45        self.realm_secret = Some(secret.into());
46        self
47    }
48
49    /// Initial registration: authenticate with MFR manifest
50    ///
51    /// Sends a RegisterRequest (containing manifest_raw + mfr_signature),
52    /// receives a RegisterResponse.
53    /// On initial registration, AIS returns a PSK in the response for subsequent renewals.
54    pub async fn register_with_manifest(
55        &self,
56        req: RegisterRequest,
57    ) -> HyperResult<RegisterResponse> {
58        info!(
59            endpoint = %self.endpoint,
60            "initial registration: registering with AIS via MFR manifest"
61        );
62        self.do_register(req).await
63    }
64
65    /// Renewal registration: authenticate with PSK
66    ///
67    /// Sends a RegisterRequest (containing psk_token),
68    /// receives a RegisterResponse with a new credential.
69    pub async fn register_with_psk(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
70        debug!(
71            endpoint = %self.endpoint,
72            "PSK renewal: renewing credential via existing PSK"
73        );
74        self.do_register(req).await
75    }
76
77    /// Linked registration: authenticate with realm authorization.
78    ///
79    /// Sends a RegisterRequest marked as linked source mode. AIS authorizes it
80    /// using the realm secret header instead of MFR package identity.
81    pub async fn register_linked(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
82        info!(
83            endpoint = %self.endpoint,
84            "linked registration: registering with AIS via realm authorization"
85        );
86        self.do_register(req).await
87    }
88
89    /// Send POST /register request, common logic
90    ///
91    /// Encodes a RegisterRequest as protobuf and POSTs it to `{endpoint}/register`,
92    /// then decodes the response as RegisterResponse.
93    async fn do_register(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
94        let url = format!("{}/register", self.endpoint);
95
96        // encode as protobuf bytes
97        let body = req.encode_to_vec();
98
99        debug!(url = %url, body_len = body.len(), "sending AIS register request");
100
101        let mut request_builder = self
102            .http
103            .post(&url)
104            .header("Content-Type", "application/x-protobuf")
105            .header("Accept", "application/x-protobuf");
106
107        // Include realm secret header if configured
108        if let Some(ref secret) = self.realm_secret {
109            request_builder = request_builder.header("x-actrix-realm-secret", secret);
110        }
111
112        let response = request_builder.body(body).send().await.map_err(|e| {
113            error!(url = %url, error = %e, "AIS HTTP request failed");
114            HyperError::AisBootstrapFailed(format!("HTTP request failed: {e}"))
115        })?;
116
117        let status = response.status();
118        if !status.is_success() {
119            warn!(url = %url, status = %status, "AIS returned non-2xx status");
120            return Err(HyperError::AisBootstrapFailed(format!(
121                "AIS returned error status: {status}"
122            )));
123        }
124
125        let bytes = response.bytes().await.map_err(|e| {
126            error!(url = %url, error = %e, "failed to read AIS response body");
127            HyperError::AisBootstrapFailed(format!("failed to read response body: {e}"))
128        })?;
129
130        debug!(url = %url, response_len = bytes.len(), "received AIS response");
131
132        let resp = RegisterResponse::decode(bytes.as_ref()).map_err(|e| {
133            error!(url = %url, error = %e, "failed to decode AIS RegisterResponse");
134            HyperError::AisBootstrapFailed(format!("response protobuf decode failed: {e}"))
135        })?;
136
137        Ok(resp)
138    }
139}