use crate::{GatewayError, GatewayRequestFence, GatewayResult, MeshPeerRequest, MeshPeerResponse};
use appcore_peer_rpc::{json_payload_hash, v2::PeerRpcWireErrorV2};
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Formatter};
pub const GATEWAY_FEDERATION_SCHEMA_V2: &str = "appcore.gateway.federation.v2";
pub const GATEWAY_FEDERATION_PATH_V2: &str = "/v2/gateway/federation/mesh";
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GatewayFederationRequestV2 {
pub schema: String,
pub fence: GatewayRequestFence,
pub request: MeshPeerRequest,
}
impl GatewayFederationRequestV2 {
pub fn new(fence: GatewayRequestFence, request: MeshPeerRequest) -> GatewayResult<Self> {
let value = Self {
schema: GATEWAY_FEDERATION_SCHEMA_V2.to_string(),
fence,
request,
};
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> GatewayResult<()> {
validate_fence(&self.fence)?;
self.request
.validate_schema()
.map_err(|_| protocol_error("federation inner request is invalid"))?;
if self.schema != GATEWAY_FEDERATION_SCHEMA_V2
|| self.fence.origin_instance_id == self.fence.target_instance_id
|| self.fence.request_id != self.request.request_id
|| self.fence.tenant_id != self.request.target_tenant_id
|| self.fence.target_core_id != self.request.target_core_id
{
return Err(protocol_error("federation routing identity is invalid"));
}
if let Ok(envelope) = self.request.peer_envelope() {
if envelope.cluster_id != self.fence.target_cluster_id {
return Err(protocol_error("federation cluster identity is invalid"));
}
}
Ok(())
}
pub fn body_hash(&self) -> GatewayResult<String> {
self.validate()?;
json_payload_hash(self).map_err(|_| protocol_error("federation request encoding failed"))
}
}
impl Debug for GatewayFederationRequestV2 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("GatewayFederationRequestV2")
.field("schema", &self.schema)
.field("fence", &self.fence)
.field("inner_body_bytes", &self.request.body.len())
.field("has_inner_credential", &self.request.bearer_token.is_some())
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GatewayFederationResponseV2 {
pub schema: String,
pub fence: GatewayRequestFence,
pub response: Option<MeshPeerResponse>,
pub error: Option<PeerRpcWireErrorV2>,
}
impl GatewayFederationResponseV2 {
pub fn ok(fence: GatewayRequestFence, response: MeshPeerResponse) -> Self {
Self {
schema: GATEWAY_FEDERATION_SCHEMA_V2.to_string(),
fence,
response: Some(response),
error: None,
}
}
pub fn rejected(fence: GatewayRequestFence, error: PeerRpcWireErrorV2) -> Self {
Self {
schema: GATEWAY_FEDERATION_SCHEMA_V2.to_string(),
fence,
response: None,
error: Some(error),
}
}
pub fn validate_for_request(&self, request: &GatewayFederationRequestV2) -> GatewayResult<()> {
if self.schema != GATEWAY_FEDERATION_SCHEMA_V2 || self.fence != request.fence {
return Err(protocol_error("federation response identity is invalid"));
}
match (&self.response, &self.error) {
(Some(response), None) => response
.validate_for_request(
&request.request.request_id,
request.request.max_response_bytes,
)
.map_err(|_| protocol_error("federation response body is invalid")),
(None, Some(error)) => {
error
.clone()
.validated()
.map_err(|_| protocol_error("federation rejection metadata is invalid"))?;
if error.request_id.as_deref() != Some(request.request.request_id.as_str()) {
return Err(protocol_error("federation rejection identity is invalid"));
}
Ok(())
}
_ => Err(protocol_error("federation response shape is invalid")),
}
}
}
impl Debug for GatewayFederationResponseV2 {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("GatewayFederationResponseV2")
.field("schema", &self.schema)
.field("fence", &self.fence)
.field("has_response", &self.response.is_some())
.field("error", &self.error)
.finish()
}
}
fn protocol_error(message: &'static str) -> GatewayError {
GatewayError::Protocol(message.to_string())
}
fn validate_fence(fence: &GatewayRequestFence) -> GatewayResult<()> {
if fence.request_id.is_empty()
|| fence.request_id.len() > 128
|| !fence
.request_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':'))
|| fence.origin_epoch == 0
|| fence.target_epoch == 0
|| fence.worker_generation == 0
|| fence.expires_at_ms == 0
{
return Err(protocol_error("federation request fence is invalid"));
}
Ok(())
}
#[cfg(test)]
#[path = "federation_tests.rs"]
mod tests;