use std::net::SocketAddr;
use alpine::crypto::identity::NodeCredentials;
use alpine::messages::{CapabilitySet, DeviceIdentity};
use crate::discovery::{DiscoveryAttempt, DiscoveryOutcome, DiscoveryRunReport};
use crate::error::AlpineSdkError;
use crate::session::{AlpineClient, ProbeOptions, ProbeState};
use std::time::Instant;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct ConnectPolicy {
pub require_trust: bool,
pub require_probe: bool,
pub allow_degraded: bool,
pub probe_options: ProbeOptions,
}
impl Default for ConnectPolicy {
fn default() -> Self {
Self {
require_trust: true,
require_probe: true,
allow_degraded: false,
probe_options: ProbeOptions::default(),
}
}
}
#[derive(Debug)]
pub enum HandshakeReportResult {
Connected(AlpineClient),
Failed(AlpineSdkError),
}
#[derive(Debug, Clone)]
pub struct TraceStep {
pub label: String,
pub elapsed_ms: u128,
}
#[derive(Debug, Clone)]
pub struct TraceTimeline {
pub steps: Vec<TraceStep>,
}
impl TraceTimeline {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn push(&mut self, label: impl Into<String>, started: Instant) {
self.steps.push(TraceStep {
label: label.into(),
elapsed_ms: started.elapsed().as_millis(),
});
}
}
#[must_use]
#[derive(Debug)]
pub struct HandshakeReport {
pub run_id: Option<String>,
pub remote_addr: Option<SocketAddr>,
pub device_id: Option<String>,
pub policy: ConnectPolicy,
pub probe: Option<crate::session::ProbeResult>,
pub discovery_attempts: Vec<DiscoveryAttempt>,
pub result: HandshakeReportResult,
pub timeline: TraceTimeline,
}
impl HandshakeReport {
pub fn into_result(self) -> Result<AlpineClient, AlpineSdkError> {
match self.result {
HandshakeReportResult::Connected(client) => Ok(client),
HandshakeReportResult::Failed(err) => Err(err),
}
}
}
pub async fn connect(
local_addr: SocketAddr,
remote_addr: SocketAddr,
identity: DeviceIdentity,
capabilities: CapabilitySet,
credentials: NodeCredentials,
) -> Result<AlpineClient, AlpineSdkError> {
AlpineClient::connect(local_addr, remote_addr, identity, capabilities, credentials).await
}
pub async fn connect_with_policy(
local_addr: SocketAddr,
outcome: DiscoveryOutcome,
credentials: NodeCredentials,
policy: ConnectPolicy,
) -> Result<AlpineClient, AlpineSdkError> {
let run_id = outcome.run_id.clone();
let outcome = if policy.require_trust {
outcome.require_trusted()?.outcome
} else {
outcome
};
let protocol_report = outcome.protocol_report();
if !protocol_report.compatible {
return Err(AlpineSdkError::IncompatibleProtocol(protocol_report.note));
}
let identity = DeviceIdentity {
device_id: outcome.reply.device_id.clone(),
manufacturer_id: outcome.reply.manufacturer_id.clone(),
model_id: outcome.reply.model_id.clone(),
hardware_rev: outcome.reply.hardware_rev.clone(),
firmware_rev: outcome.reply.firmware_rev.clone(),
};
let capabilities = outcome.reply.capabilities.clone();
let client = AlpineClient::connect(
local_addr,
outcome.peer,
identity,
capabilities,
credentials,
)
.await
.map_err(|err| {
warn_clock_skew_if_needed(&err);
err
})?;
let mut client = client;
client.set_run_id(run_id);
if policy.require_probe {
let probe = client
.probe_status_with_options(policy.probe_options.clone())
.await;
let acceptable = match probe.state {
ProbeState::Online => true,
ProbeState::Degraded => policy.allow_degraded,
ProbeState::Offline => false,
};
if !acceptable {
let detail = probe
.detail
.or_else(|| probe.errors.first().map(|err| err.message.clone()))
.unwrap_or_else(|| format!("probe state {:?}", probe.state));
return Err(AlpineSdkError::ProbeFailed(detail));
}
}
Ok(client)
}
pub async fn connect_with_policy_report(
local_addr: SocketAddr,
outcome: DiscoveryOutcome,
credentials: NodeCredentials,
policy: ConnectPolicy,
) -> HandshakeReport {
let started = Instant::now();
let mut timeline = TraceTimeline::new();
let device_id = Some(outcome.reply.device_id.clone());
let remote_addr = Some(outcome.peer);
let run_id = Some(outcome.run_id.clone());
let mut probe = None;
let outcome = if policy.require_trust {
match outcome.require_trusted() {
Ok(trusted) => trusted.outcome,
Err(err) => {
timeline.push("trust_failed", started);
return HandshakeReport {
run_id,
remote_addr,
device_id,
policy,
probe: None,
discovery_attempts: Vec::new(),
result: HandshakeReportResult::Failed(err),
timeline,
};
}
}
} else {
timeline.push("trust_skipped", started);
outcome
};
let protocol_report = outcome.protocol_report();
if !protocol_report.compatible {
timeline.push("protocol_mismatch", started);
return HandshakeReport {
run_id,
remote_addr,
device_id,
policy,
probe: None,
discovery_attempts: Vec::new(),
result: HandshakeReportResult::Failed(AlpineSdkError::IncompatibleProtocol(
protocol_report.note,
)),
timeline,
};
}
let identity = DeviceIdentity {
device_id: outcome.reply.device_id.clone(),
manufacturer_id: outcome.reply.manufacturer_id.clone(),
model_id: outcome.reply.model_id.clone(),
hardware_rev: outcome.reply.hardware_rev.clone(),
firmware_rev: outcome.reply.firmware_rev.clone(),
};
let capabilities = outcome.reply.capabilities.clone();
let client = match AlpineClient::connect(
local_addr,
outcome.peer,
identity,
capabilities,
credentials,
)
.await
{
Ok(client) => client,
Err(err) => {
warn_clock_skew_if_needed(&err);
timeline.push("handshake_failed", started);
return HandshakeReport {
run_id,
remote_addr,
device_id,
policy,
probe: None,
discovery_attempts: Vec::new(),
result: HandshakeReportResult::Failed(err),
timeline,
};
}
};
let mut client = client;
if let Some(run_id) = run_id.clone() {
client.set_run_id(run_id);
}
timeline.push("handshake_connected", started);
let result = if policy.require_probe {
let probe_result = client
.probe_status_with_options(policy.probe_options.clone())
.await;
let acceptable = match probe_result.state {
ProbeState::Online => true,
ProbeState::Degraded => policy.allow_degraded,
ProbeState::Offline => false,
};
probe = Some(probe_result.clone());
if !acceptable {
let detail = probe_result
.detail
.clone()
.or_else(|| probe_result.errors.first().map(|err| err.message.clone()))
.unwrap_or_else(|| format!("probe state {:?}", probe_result.state));
client.close().await;
timeline.push("probe_failed", started);
HandshakeReportResult::Failed(AlpineSdkError::ProbeFailed(detail))
} else {
timeline.push("probe_ok", started);
HandshakeReportResult::Connected(client)
}
} else {
timeline.push("probe_skipped", started);
HandshakeReportResult::Connected(client)
};
HandshakeReport {
run_id,
remote_addr,
device_id,
policy,
probe,
discovery_attempts: Vec::new(),
result,
timeline,
}
}
pub async fn connect_with_policy_from_report(
local_addr: SocketAddr,
report: DiscoveryRunReport,
credentials: NodeCredentials,
policy: ConnectPolicy,
) -> Result<AlpineClient, AlpineSdkError> {
let outcome = match report.result {
Ok(outcome) => outcome,
Err(err) => {
let attempts = format_attempts(&report.attempts);
return Err(AlpineSdkError::DiscoveryFailed(format!(
"{}; attempts: {}",
err, attempts
)));
}
};
match connect_with_policy(local_addr, outcome, credentials, policy).await {
Ok(client) => Ok(client),
Err(AlpineSdkError::ProbeFailed(detail)) => {
let attempts = format_attempts(&report.attempts);
Err(AlpineSdkError::ProbeFailed(format!(
"{}; attempts: {}",
detail, attempts
)))
}
Err(err) => Err(err),
}
}
pub async fn connect_with_policy_from_report_report(
local_addr: SocketAddr,
report: DiscoveryRunReport,
credentials: NodeCredentials,
policy: ConnectPolicy,
) -> HandshakeReport {
let attempts = report.attempts.clone();
match report.result {
Ok(outcome) => {
let mut handshake =
connect_with_policy_report(local_addr, outcome, credentials, policy).await;
handshake.discovery_attempts = attempts;
handshake
}
Err(err) => {
let detail = format_attempts(&attempts);
HandshakeReport {
run_id: None,
remote_addr: None,
device_id: None,
policy,
probe: None,
discovery_attempts: attempts,
result: HandshakeReportResult::Failed(AlpineSdkError::DiscoveryFailed(format!(
"{}; attempts: {}",
err, detail
))),
timeline: TraceTimeline::new(),
}
}
}
}
fn format_attempts(attempts: &[DiscoveryAttempt]) -> String {
if attempts.is_empty() {
return "none".into();
}
let mut rendered = Vec::new();
for attempt in attempts {
let error = attempt.error.as_ref().map(|err| err.label).unwrap_or("ok");
rendered.push(format!(
"{} {} {}",
attempt.mode.as_str(),
attempt.target,
error
));
}
rendered.join(", ")
}
fn warn_clock_skew_if_needed(err: &AlpineSdkError) {
if let AlpineSdkError::HandshakeFailed(detail) = err {
let lowered = detail.to_ascii_lowercase();
if lowered.contains("signature")
|| lowered.contains("expired")
|| lowered.contains("timestamp")
{
warn!(
"[ALPINE][HANDSHAKE][WARN] signature validation failed; clock skew may be causing auth errors"
);
}
}
}