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