#[cfg_attr(not(feature = "sc-tls"), allow(dead_code))]
pub(crate) mod credentials;
pub(crate) mod doctor;
use std::time::Duration;
use bacnet_transport::sc::{
ScActiveHub, ScConnectError, ScConnectionState, ScDialStage, ScDisconnectCause, ScTlsFailure,
ScWebSocketErrorKind,
};
use bacnet_transport::sc_frame::ScFunction;
use bacnet_types::enums::{ErrorClass, ErrorCode};
use bacnet_types::error::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ScIdentity {
pub(crate) primary: String,
pub(crate) failover: Option<String>,
pub(crate) vmac: [u8; 6],
pub(crate) uuid: [u8; 16],
}
impl ScIdentity {
pub(crate) fn url(&self, hub: ScActiveHub) -> Option<&str> {
match hub {
ScActiveHub::Primary => Some(&self.primary),
ScActiveHub::Failover => self.failover.as_deref(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LinkSnapshot {
pub(crate) state: ScConnectionState,
pub(crate) hub: Option<ScActiveHub>,
pub(crate) hub_vmac: Option<[u8; 6]>,
pub(crate) heartbeat_age: Option<Duration>,
pub(crate) heartbeat_rtt: Option<Duration>,
}
impl From<bacnet_transport::sc::ScLinkStatus> for LinkSnapshot {
fn from(status: bacnet_transport::sc::ScLinkStatus) -> Self {
Self {
state: status.state,
hub: status.active_hub,
hub_vmac: status.hub_vmac,
heartbeat_age: status.heartbeat_age,
heartbeat_rtt: status.heartbeat_rtt,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Fault {
WebSocket(ScWebSocketErrorKind),
Tls(ScTlsFailure),
Nak {
class: ErrorClass,
code: ErrorCode,
reseeded: bool,
},
DialTimeout(Option<ScDialStage>),
ConnectAcceptTimeout,
UpgradeRefused(u16),
HeartbeatTimeout,
PeerClose,
LocalClose,
BadReply,
Other(String),
}
impl Fault {
pub(crate) fn from_error(error: &Error) -> Self {
let reseeded = matches!(
ScConnectError::from_error(error),
Some(ScConnectError::HandshakeNak {
duplicate_vmac_reseeded: true,
..
})
);
match Self::from(ScDisconnectCause::from_error(error)) {
Self::Nak { class, code, .. } => Self::Nak {
class,
code,
reseeded,
},
other => other,
}
}
pub(crate) fn is_duplicate_vmac(&self) -> bool {
matches!(self, Self::Nak { code, .. } if *code == ErrorCode::NODE_DUPLICATE_VMAC)
}
}
impl From<ScDisconnectCause> for Fault {
fn from(cause: ScDisconnectCause) -> Self {
match cause {
ScDisconnectCause::LocalClose => Self::LocalClose,
ScDisconnectCause::PeerClose => Self::PeerClose,
ScDisconnectCause::HeartbeatTimeout | ScDisconnectCause::RejectionExpired => {
Self::HeartbeatTimeout
}
ScDisconnectCause::WebSocket(kind) => Self::WebSocket(kind),
ScDisconnectCause::Tls(failure) => Self::Tls(failure),
ScDisconnectCause::Nak {
result_for,
error_class,
error_code,
..
} => Self::Nak {
class: error_class,
code: error_code,
reseeded: error_code == ErrorCode::NODE_DUPLICATE_VMAC
&& result_for == ScFunction::ConnectRequest,
},
ScDisconnectCause::DialTimeout { stage, .. } => Self::DialTimeout(stage),
ScDisconnectCause::ConnectAcceptTimeout => Self::ConnectAcceptTimeout,
ScDisconnectCause::UpgradeRefused { http_status, .. } => {
Self::UpgradeRefused(http_status)
}
ScDisconnectCause::MalformedResult | ScDisconnectCause::UnexpectedConnectReply => {
Self::BadReply
}
ScDisconnectCause::Other(text) => Self::Other(text),
other => Self::Other(format!("{other:?}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CertSummary {
pub(crate) subject: String,
pub(crate) issuer: String,
pub(crate) not_after_unix: i64,
pub(crate) not_after: String,
pub(crate) signature_algorithm: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ScFailure {
pub(crate) fault: Fault,
pub(crate) detail: String,
pub(crate) presented: Vec<CertSummary>,
}
impl ScFailure {
pub(crate) fn from_error(error: &Error) -> Self {
Self {
fault: Fault::from_error(error),
detail: error.to_string(),
presented: presented(error),
}
}
}
#[cfg(feature = "sc-tls")]
fn presented(error: &Error) -> Vec<CertSummary> {
ScConnectError::from_error(error)
.and_then(ScConnectError::untrusted_peer_info)
.map(|info| summarize(info.certificates()))
.unwrap_or_default()
}
#[cfg(not(feature = "sc-tls"))]
fn presented(_: &Error) -> Vec<CertSummary> {
Vec::new()
}
#[cfg(feature = "sc-tls")]
pub(crate) fn summarize(
certificates: &[bacnet_transport::sc_tls::ScPeerCertificate],
) -> Vec<CertSummary> {
certificates
.iter()
.map(|certificate| match certificate.metadata() {
Some(meta) => CertSummary {
subject: meta.subject().to_string(),
issuer: meta.issuer().to_string(),
not_after_unix: meta.not_after_unix(),
not_after: crate::sc_check::date(meta.not_after_unix()),
signature_algorithm: meta.signature_algorithm_oid().to_string(),
},
None => CertSummary {
subject: "(unparsed certificate)".into(),
issuer: String::new(),
not_after_unix: 0,
not_after: String::new(),
signature_algorithm: String::new(),
},
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum LinkEvent {
Connected {
hub: ScActiveHub,
hub_vmac: [u8; 6],
},
Disconnected {
hub: ScActiveHub,
fault: Fault,
},
ConnectFailed {
hub: ScActiveHub,
fault: Fault,
},
Reconnecting {
hub: ScActiveHub,
attempt: u32,
backoff: Duration,
},
FailoverEngaged,
PrimaryRestored,
PrimaryRestoreFailed {
fault: Fault,
},
GaveUp {
attempts: u32,
},
}
impl LinkEvent {
pub(crate) fn from_event(event: bacnet_transport::sc::ScEvent) -> Option<Self> {
use bacnet_transport::sc::ScEvent as E;
Some(match event {
E::Connected { hub, hub_vmac, .. } => Self::Connected { hub, hub_vmac },
E::Disconnected { hub, cause, .. } => Self::Disconnected {
hub,
fault: cause.into(),
},
E::ConnectFailed { hub, cause, .. } => Self::ConnectFailed {
hub,
fault: cause.into(),
},
E::ReconnectAttempt {
hub,
attempt,
backoff,
..
} => Self::Reconnecting {
hub,
attempt,
backoff,
},
E::FailoverEngaged => Self::FailoverEngaged,
E::PrimaryRestored => Self::PrimaryRestored,
E::PrimaryRestoreFailed { cause, .. } => Self::PrimaryRestoreFailed {
fault: cause.into(),
},
E::ReconnectGaveUp { attempts, .. } => Self::GaveUp { attempts },
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScReport {
Dialing(ScIdentity),
Failed(Box<ScFailure>),
Status(LinkSnapshot),
Event(LinkEvent),
Gap {
missed: u64,
},
Chain(Vec<CertSummary>),
#[cfg_attr(not(feature = "sc-tls"), allow(dead_code))]
Credentials(Box<Result<credentials::Credentials, String>>),
}
pub(crate) fn hub_name(hub: ScActiveHub) -> &'static str {
match hub {
ScActiveHub::Primary => "primary",
ScActiveHub::Failover => "failover",
}
}
pub(crate) fn vmac_text(vmac: &[u8]) -> String {
vmac.iter()
.map(|octet| format!("{octet:02x}"))
.collect::<Vec<_>>()
.join(":")
}
pub(crate) fn uuid_text(uuid: &[u8; 16]) -> String {
let hex: String = uuid.iter().map(|octet| format!("{octet:02x}")).collect();
format!(
"{}-{}-{}-{}-{}",
&hex[..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..]
)
}
#[cfg(test)]
mod tests {
use super::*;
use bacnet_transport::sc::{ScCertificateRejection, ScTlsAlert};
#[test]
fn faults_sort_from_connect_errors() {
let timeout = Error::Timeout(Duration::from_secs(10));
assert_eq!(Fault::from_error(&timeout), Fault::ConnectAcceptTimeout);
let other = Error::Encoding("TLS config error".into());
assert!(matches!(Fault::from_error(&other), Fault::Other(_)));
let tls = ScDisconnectCause::Tls(ScTlsFailure::PeerCertificate(
ScCertificateRejection::UnknownIssuer,
));
assert_eq!(
Fault::from(tls),
Fault::Tls(ScTlsFailure::PeerCertificate(
ScCertificateRejection::UnknownIssuer
))
);
let alert = ScDisconnectCause::Tls(ScTlsFailure::AlertReceived(ScTlsAlert::UnknownCa));
assert!(!Fault::from(alert).is_duplicate_vmac());
let duplicate = Fault::Nak {
class: ErrorClass::COMMUNICATION,
code: ErrorCode::NODE_DUPLICATE_VMAC,
reseeded: true,
};
assert!(duplicate.is_duplicate_vmac());
}
#[test]
fn identities_print_as_operators_write_them() {
assert_eq!(vmac_text(&[2, 0, 0, 0, 0, 0x1a]), "02:00:00:00:00:1a");
let uuid = [
0x6f, 0x1c, 0x2a, 0x9e, 0x44, 0x07, 0x4b, 0x3d, 0x9a, 0x51, 0x0e, 0x7c, 0x88, 0x21,
0xd4, 0x0b,
];
assert_eq!(uuid_text(&uuid), "6f1c2a9e-4407-4b3d-9a51-0e7c8821d40b");
}
}