use serde::{Deserialize, Serialize};
use thiserror::Error;
use hessra_config::{HessraConfig, Protocol};
pub fn parse_server_address(address: &str) -> (String, Option<u16>) {
let address = address.trim();
let without_protocol = address
.strip_prefix("https://")
.or_else(|| address.strip_prefix("http://"))
.unwrap_or(address);
let host_port = without_protocol
.split('/')
.next()
.unwrap_or(without_protocol);
if host_port.starts_with('[') {
if let Some(bracket_end) = host_port.find(']') {
let host = &host_port[1..bracket_end]; let after_bracket = &host_port[bracket_end + 1..];
if let Some(port_str) = after_bracket.strip_prefix(':') {
if let Ok(port) = port_str.parse::<u16>() {
return (host.to_string(), Some(port));
}
}
return (host.to_string(), None);
}
return (host_port.trim_start_matches('[').to_string(), None);
}
let colon_count = host_port.chars().filter(|c| *c == ':').count();
if colon_count == 1 {
let parts: Vec<&str> = host_port.splitn(2, ':').collect();
if parts.len() == 2 {
if let Ok(port) = parts[1].parse::<u16>() {
return (parts[0].to_string(), Some(port));
}
}
}
(host_port.to_string(), None)
}
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP client error: {0}")]
HttpClient(#[from] reqwest::Error),
#[error("SSL configuration error: {0}")]
SslConfig(String),
#[error("Invalid response: {0}")]
InvalidResponse(String),
#[error("Token request error: {0}")]
TokenRequest(String),
#[error("Token verification error: {0}")]
TokenVerification(String),
#[error("Service chain error: {0}")]
ServiceChain(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Signoff failed: {0}")]
SignoffFailed(String),
#[error("Missing signoff configuration for service: {0}")]
MissingSignoffConfig(String),
#[error("Invalid signoff response from {service}: {reason}")]
InvalidSignoffResponse { service: String, reason: String },
#[error("Signoff collection incomplete: {missing_signoffs} signoffs remaining")]
IncompleteSignoffs { missing_signoffs: usize },
}
#[derive(Serialize, Deserialize)]
pub struct TokenRequest {
pub resource: String,
pub operation: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyTokenRequest {
pub token: String,
pub subject: String,
pub resource: String,
pub operation: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SignoffInfo {
pub component: String,
pub authorization_service: String,
pub public_key: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SignTokenRequest {
pub token: String,
pub resource: String,
pub operation: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SignTokenResponse {
pub response_msg: String,
pub signed_token: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TokenResponse {
pub response_msg: String,
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_signoffs: Option<Vec<SignoffInfo>>,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyTokenResponse {
pub response_msg: String,
}
#[derive(Serialize, Deserialize)]
pub struct PublicKeyResponse {
pub response_msg: String,
pub public_key: String,
}
#[derive(Serialize, Deserialize)]
pub struct CaCertResponse {
pub response_msg: String,
pub ca_cert_pem: String,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyServiceChainTokenRequest {
pub token: String,
pub subject: String,
pub resource: String,
pub component: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct IdentityTokenRequest {
pub identifier: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct RefreshIdentityTokenRequest {
pub current_token: String,
pub identifier: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct IdentityTokenResponse {
pub response_msg: String,
pub token: Option<String>,
pub expires_in: Option<u64>,
pub identity: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct MintIdentityTokenRequest {
pub subject: String,
pub duration: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MintIdentityTokenResponse {
pub response_msg: String,
pub token: Option<String>,
pub expires_in: Option<u64>,
pub identity: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StubTokenRequest {
pub target_identity: String,
pub resource: String,
pub operation: String,
pub prefix_attenuator_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct StubTokenResponse {
pub response_msg: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_in: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_identity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_attenuator_key: Option<String>,
}
#[derive(Clone)]
pub struct BaseConfig {
pub base_url: String,
pub port: Option<u16>,
pub mtls_key: Option<String>,
pub mtls_cert: Option<String>,
pub server_ca: String,
pub public_key: Option<String>,
pub personal_keypair: Option<String>,
}
impl BaseConfig {
pub fn get_base_url(&self) -> String {
let (host, embedded_port) = parse_server_address(&self.base_url);
let resolved_port = self.port.or(embedded_port);
match resolved_port {
Some(port) => format!("{host}:{port}"),
None => host,
}
}
}
pub struct Http1Client {
config: BaseConfig,
client: reqwest::Client,
}
impl Http1Client {
pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
let certs =
reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
})?;
let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
for cert in certs {
client_builder = client_builder.add_root_certificate(cert);
}
if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
let identity_str = format!("{cert}{key}");
let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
ApiError::SslConfig(format!(
"Failed to create identity from certificate and key: {e}"
))
})?;
client_builder = client_builder.identity(identity);
}
let client = client_builder
.build()
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
Ok(Self { config, client })
}
pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
where
T: Serialize,
R: for<'de> Deserialize<'de>,
{
let base_url = self.config.get_base_url();
let url = format!("https://{base_url}/{endpoint}");
let response = self
.client
.post(&url)
.json(request_body)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<R>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result)
}
pub async fn send_request_with_auth<T, R>(
&self,
endpoint: &str,
request_body: &T,
auth_header: &str,
) -> Result<R, ApiError>
where
T: Serialize,
R: for<'de> Deserialize<'de>,
{
let base_url = self.config.get_base_url();
let url = format!("https://{base_url}/{endpoint}");
let response = self
.client
.post(&url)
.header("Authorization", auth_header)
.json(request_body)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<R>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result)
}
}
#[cfg(feature = "http3")]
pub struct Http3Client {
config: BaseConfig,
client: reqwest::Client,
}
#[cfg(feature = "http3")]
impl Http3Client {
pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
let certs =
reqwest::Certificate::from_pem_bundle(config.server_ca.as_bytes()).map_err(|e| {
ApiError::SslConfig(format!("Failed to parse CA certificate chain: {e}"))
})?;
let mut client_builder = reqwest::ClientBuilder::new()
.use_rustls_tls()
.http3_prior_knowledge();
for cert in certs {
client_builder = client_builder.add_root_certificate(cert);
}
if let (Some(cert), Some(key)) = (&config.mtls_cert, &config.mtls_key) {
let identity_str = format!("{}{}", cert, key);
let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
ApiError::SslConfig(format!(
"Failed to create identity from certificate and key: {e}"
))
})?;
client_builder = client_builder.identity(identity);
}
let client = client_builder
.build()
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
Ok(Self { config, client })
}
pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
where
T: Serialize,
R: for<'de> Deserialize<'de>,
{
let base_url = self.config.get_base_url();
let url = format!("https://{base_url}/{endpoint}");
let response = self
.client
.post(&url)
.json(request_body)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<R>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result)
}
pub async fn send_request_with_auth<T, R>(
&self,
endpoint: &str,
request_body: &T,
auth_header: &str,
) -> Result<R, ApiError>
where
T: Serialize,
R: for<'de> Deserialize<'de>,
{
let base_url = self.config.get_base_url();
let url = format!("https://{base_url}/{endpoint}");
let response = self
.client
.post(&url)
.header("Authorization", auth_header)
.json(request_body)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<R>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result)
}
}
pub enum HessraClient {
Http1(Http1Client),
#[cfg(feature = "http3")]
Http3(Http3Client),
}
pub struct HessraClientBuilder {
config: BaseConfig,
protocol: hessra_config::Protocol,
}
impl HessraClientBuilder {
pub fn new() -> Self {
Self {
config: BaseConfig {
base_url: String::new(),
port: None,
mtls_key: None,
mtls_cert: None,
server_ca: String::new(),
public_key: None,
personal_keypair: None,
},
protocol: Protocol::Http1,
}
}
pub fn from_config(mut self, config: &HessraConfig) -> Self {
self.config.base_url = config.base_url.clone();
self.config.port = config.port;
self.config.mtls_key = config.mtls_key.clone();
self.config.mtls_cert = config.mtls_cert.clone();
self.config.server_ca = config.server_ca.clone();
self.config.public_key = config.public_key.clone();
self.config.personal_keypair = config.personal_keypair.clone();
self.protocol = config.protocol.clone();
self
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.config.base_url = base_url.into();
self
}
pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
self.config.mtls_key = Some(mtls_key.into());
self
}
pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
self.config.mtls_cert = Some(mtls_cert.into());
self
}
pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
self.config.server_ca = server_ca.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.config.port = Some(port);
self
}
pub fn protocol(mut self, protocol: Protocol) -> Self {
self.protocol = protocol;
self
}
pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
self.config.public_key = Some(public_key.into());
self
}
pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
self.config.personal_keypair = Some(keypair.into());
self
}
fn build_http1(&self) -> Result<Http1Client, ApiError> {
Http1Client::new(self.config.clone())
}
#[cfg(feature = "http3")]
fn build_http3(&self) -> Result<Http3Client, ApiError> {
Http3Client::new(self.config.clone())
}
pub fn build(self) -> Result<HessraClient, ApiError> {
match self.protocol {
Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
#[cfg(feature = "http3")]
Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
#[allow(unreachable_patterns)]
_ => Err(ApiError::Internal("Unsupported protocol".to_string())),
}
}
}
impl Default for HessraClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl HessraClient {
pub fn builder() -> HessraClientBuilder {
HessraClientBuilder::new()
}
pub async fn fetch_public_key(
base_url: impl Into<String>,
port: Option<u16>,
server_ca: impl Into<String>,
) -> Result<String, ApiError> {
let base_url_str = base_url.into();
let server_ca = server_ca.into();
let (host, embedded_port) = parse_server_address(&base_url_str);
let resolved_port = embedded_port.or(port);
let cert_pem = server_ca.as_bytes();
let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
let mut client_builder = reqwest::ClientBuilder::new().use_rustls_tls();
for cert in certs {
client_builder = client_builder.add_root_certificate(cert);
}
let client = client_builder
.build()
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
let url = match resolved_port {
Some(port) => format!("https://{host}:{port}/public_key"),
None => format!("https://{host}/public_key"),
};
let response = client
.get(&url)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<PublicKeyResponse>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result.public_key)
}
pub async fn fetch_ca_cert(
base_url: impl Into<String>,
port: Option<u16>,
) -> Result<String, ApiError> {
let base_url_str = base_url.into();
let (host, embedded_port) = parse_server_address(&base_url_str);
let resolved_port = embedded_port.or(port);
let client = reqwest::ClientBuilder::new()
.use_rustls_tls()
.build()
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
let url = match resolved_port {
Some(port) => format!("https://{host}:{port}/ca_cert"),
None => format!("https://{host}/ca_cert"),
};
let response = client
.get(&url)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<CaCertResponse>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
if result.ca_cert_pem.is_empty() {
return Err(ApiError::InvalidResponse(
"Server returned empty CA certificate".to_string(),
));
}
if !result.ca_cert_pem.contains("-----BEGIN CERTIFICATE-----") {
return Err(ApiError::InvalidResponse(
"Server returned invalid PEM format".to_string(),
));
}
Ok(result.ca_cert_pem)
}
#[cfg(feature = "http3")]
pub async fn fetch_public_key_http3(
base_url: impl Into<String>,
port: Option<u16>,
server_ca: impl Into<String>,
) -> Result<String, ApiError> {
let base_url_str = base_url.into();
let server_ca = server_ca.into();
let (host, embedded_port) = parse_server_address(&base_url_str);
let resolved_port = embedded_port.or(port);
let cert_pem = server_ca.as_bytes();
let certs = reqwest::Certificate::from_pem_bundle(cert_pem)
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
let mut client_builder = reqwest::ClientBuilder::new()
.use_rustls_tls()
.http3_prior_knowledge();
for cert in certs {
client_builder = client_builder.add_root_certificate(cert);
}
let client = client_builder
.build()
.map_err(|e| ApiError::SslConfig(e.to_string()))?;
let url = match resolved_port {
Some(port) => format!("https://{host}:{port}/public_key"),
None => format!("https://{host}/public_key"),
};
let response = client
.get(&url)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
let result = response
.json::<PublicKeyResponse>()
.await
.map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {e}")))?;
Ok(result.public_key)
}
pub async fn request_token(
&self,
resource: String,
operation: String,
domain: Option<String>,
) -> Result<TokenResponse, ApiError> {
let request = TokenRequest {
resource,
operation,
domain,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, TokenResponse>("request_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, TokenResponse>("request_token", &request)
.await?
}
};
Ok(response)
}
pub async fn request_token_with_identity(
&self,
resource: String,
operation: String,
identity_token: String,
domain: Option<String>,
) -> Result<TokenResponse, ApiError> {
let request = TokenRequest {
resource,
operation,
domain,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request_with_auth::<_, TokenResponse>(
"request_token",
&request,
&format!("Bearer {identity_token}"),
)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request_with_auth::<_, TokenResponse>(
"request_token",
&request,
&format!("Bearer {identity_token}"),
)
.await?
}
};
Ok(response)
}
pub async fn request_token_simple(
&self,
resource: String,
operation: String,
) -> Result<String, ApiError> {
let response = self.request_token(resource, operation, None).await?;
match response.token {
Some(token) => Ok(token),
None => Err(ApiError::TokenRequest(format!(
"Failed to get token: {}",
response.response_msg
))),
}
}
pub async fn verify_token(
&self,
token: String,
subject: String,
resource: String,
operation: String,
) -> Result<String, ApiError> {
let request = VerifyTokenRequest {
token,
subject,
resource,
operation,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, VerifyTokenResponse>("verify_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, VerifyTokenResponse>("verify_token", &request)
.await?
}
};
Ok(response.response_msg)
}
pub async fn verify_service_chain_token(
&self,
token: String,
subject: String,
resource: String,
component: Option<String>,
) -> Result<String, ApiError> {
let request = VerifyServiceChainTokenRequest {
token,
subject,
resource,
component,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
.await?
}
};
Ok(response.response_msg)
}
pub async fn sign_token(
&self,
token: &str,
resource: &str,
operation: &str,
) -> Result<SignTokenResponse, ApiError> {
let request = SignTokenRequest {
token: token.to_string(),
resource: resource.to_string(),
operation: operation.to_string(),
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, SignTokenResponse>("sign_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, SignTokenResponse>("sign_token", &request)
.await?
}
};
Ok(response)
}
pub async fn get_public_key(&self) -> Result<String, ApiError> {
let url_path = "public_key";
let response = match self {
HessraClient::Http1(client) => {
let base_url = client.config.get_base_url();
let full_url = format!("https://{base_url}/{url_path}");
let response = client
.client
.get(&full_url)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
response.json::<PublicKeyResponse>().await.map_err(|e| {
ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
})?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
let base_url = client.config.get_base_url();
let full_url = format!("https://{base_url}/{url_path}");
let response = client
.client
.get(&full_url)
.send()
.await
.map_err(ApiError::HttpClient)?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(ApiError::InvalidResponse(format!(
"HTTP error: {status} - {error_text}"
)));
}
response.json::<PublicKeyResponse>().await.map_err(|e| {
ApiError::InvalidResponse(format!("Failed to parse response: {e}"))
})?
}
};
Ok(response.public_key)
}
pub async fn request_identity_token(
&self,
identifier: Option<String>,
) -> Result<IdentityTokenResponse, ApiError> {
let request = IdentityTokenRequest { identifier };
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, IdentityTokenResponse>("request_identity_token", &request)
.await?
}
};
Ok(response)
}
pub async fn refresh_identity_token(
&self,
current_token: String,
identifier: Option<String>,
) -> Result<IdentityTokenResponse, ApiError> {
let request = RefreshIdentityTokenRequest {
current_token,
identifier,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, IdentityTokenResponse>("refresh_identity_token", &request)
.await?
}
};
Ok(response)
}
pub async fn mint_domain_restricted_identity_token(
&self,
subject: String,
duration: Option<u64>,
) -> Result<MintIdentityTokenResponse, ApiError> {
let request = MintIdentityTokenRequest { subject, duration };
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, MintIdentityTokenResponse>("mint_identity_token", &request)
.await?
}
};
Ok(response)
}
pub async fn request_stub_token(
&self,
target_identity: String,
resource: String,
operation: String,
prefix_attenuator_key: String,
duration: Option<u64>,
) -> Result<StubTokenResponse, ApiError> {
let request = StubTokenRequest {
target_identity,
resource,
operation,
prefix_attenuator_key,
duration,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request::<_, StubTokenResponse>("request_stub", &request)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request::<_, StubTokenResponse>("request_stub", &request)
.await?
}
};
Ok(response)
}
pub async fn request_stub_token_with_identity(
&self,
target_identity: String,
resource: String,
operation: String,
prefix_attenuator_key: String,
identity_token: String,
duration: Option<u64>,
) -> Result<StubTokenResponse, ApiError> {
let request = StubTokenRequest {
target_identity,
resource,
operation,
prefix_attenuator_key,
duration,
};
let response = match self {
HessraClient::Http1(client) => {
client
.send_request_with_auth::<_, StubTokenResponse>(
"request_stub",
&request,
&format!("Bearer {identity_token}"),
)
.await?
}
#[cfg(feature = "http3")]
HessraClient::Http3(client) => {
client
.send_request_with_auth::<_, StubTokenResponse>(
"request_stub",
&request,
&format!("Bearer {identity_token}"),
)
.await?
}
};
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_config_get_base_url_with_port() {
let config = BaseConfig {
base_url: "test.hessra.net".to_string(),
port: Some(443),
mtls_key: None,
mtls_cert: None,
server_ca: "".to_string(),
public_key: None,
personal_keypair: None,
};
assert_eq!(config.get_base_url(), "test.hessra.net:443");
}
#[test]
fn test_base_config_get_base_url_without_port() {
let config = BaseConfig {
base_url: "test.hessra.net".to_string(),
port: None,
mtls_key: None,
mtls_cert: None,
server_ca: "".to_string(),
public_key: None,
personal_keypair: None,
};
assert_eq!(config.get_base_url(), "test.hessra.net");
}
#[test]
fn test_client_builder_methods() {
let builder = HessraClientBuilder::new()
.base_url("test.hessra.net")
.port(443)
.protocol(Protocol::Http1)
.mtls_cert("CERT")
.mtls_key("KEY")
.server_ca("CA")
.public_key("PUBKEY")
.personal_keypair("KEYPAIR");
assert_eq!(builder.config.base_url, "test.hessra.net");
assert_eq!(builder.config.port, Some(443));
assert_eq!(builder.config.mtls_cert, Some("CERT".to_string()));
assert_eq!(builder.config.mtls_key, Some("KEY".to_string()));
assert_eq!(builder.config.server_ca, "CA");
assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
}
#[test]
fn test_parse_server_address_ip_with_port() {
let (host, port) = parse_server_address("127.0.0.1:4433");
assert_eq!(host, "127.0.0.1");
assert_eq!(port, Some(4433));
}
#[test]
fn test_parse_server_address_ip_only() {
let (host, port) = parse_server_address("127.0.0.1");
assert_eq!(host, "127.0.0.1");
assert_eq!(port, None);
}
#[test]
fn test_parse_server_address_hostname_with_port() {
let (host, port) = parse_server_address("test.hessra.net:443");
assert_eq!(host, "test.hessra.net");
assert_eq!(port, Some(443));
}
#[test]
fn test_parse_server_address_hostname_only() {
let (host, port) = parse_server_address("test.hessra.net");
assert_eq!(host, "test.hessra.net");
assert_eq!(port, None);
}
#[test]
fn test_parse_server_address_with_https_protocol() {
let (host, port) = parse_server_address("https://example.com:8443");
assert_eq!(host, "example.com");
assert_eq!(port, Some(8443));
}
#[test]
fn test_parse_server_address_with_https_protocol_no_port() {
let (host, port) = parse_server_address("https://example.com");
assert_eq!(host, "example.com");
assert_eq!(port, None);
}
#[test]
fn test_parse_server_address_with_path() {
let (host, port) = parse_server_address("https://example.com:8443/some/path");
assert_eq!(host, "example.com");
assert_eq!(port, Some(8443));
}
#[test]
fn test_parse_server_address_ipv6_with_brackets_and_port() {
let (host, port) = parse_server_address("[::1]:8443");
assert_eq!(host, "::1");
assert_eq!(port, Some(8443));
}
#[test]
fn test_parse_server_address_ipv6_with_brackets_no_port() {
let (host, port) = parse_server_address("[::1]");
assert_eq!(host, "::1");
assert_eq!(port, None);
}
#[test]
fn test_parse_server_address_ipv6_full_with_port() {
let (host, port) = parse_server_address("[2001:db8::1]:4433");
assert_eq!(host, "2001:db8::1");
assert_eq!(port, Some(4433));
}
#[test]
fn test_parse_server_address_with_whitespace() {
let (host, port) = parse_server_address(" 127.0.0.1:4433 ");
assert_eq!(host, "127.0.0.1");
assert_eq!(port, Some(4433));
}
#[test]
fn test_base_config_get_base_url_with_embedded_port() {
let config = BaseConfig {
base_url: "127.0.0.1:4433".to_string(),
port: None, mtls_key: None,
mtls_cert: None,
server_ca: "".to_string(),
public_key: None,
personal_keypair: None,
};
assert_eq!(config.get_base_url(), "127.0.0.1:4433");
}
#[test]
fn test_base_config_get_base_url_explicit_port_overrides_embedded() {
let config = BaseConfig {
base_url: "127.0.0.1:4433".to_string(),
port: Some(8080), mtls_key: None,
mtls_cert: None,
server_ca: "".to_string(),
public_key: None,
personal_keypair: None,
};
assert_eq!(config.get_base_url(), "127.0.0.1:8080");
}
}