use serde::{Deserialize, Serialize};
use crate::pod::{CommissionError, CommissionErrorCode, PodCandidate, WifiNetwork};
pub const PROVISION_START_TARGET: &str = "/local/host/provision/start";
pub const PROVISION_CODE_TARGET: &str = "/local/host/provision/code";
pub const PROVISION_WIFI_TARGET: &str = "/local/host/provision/wifi";
pub const PROVISION_WIFI_SCAN_TARGET: &str = "/local/host/provision/wifi/scan";
pub const PROVISION_STATUS_TARGET: &str = "/local/host/provision/status";
pub const PROVISION_CHANGED_EVENT: &str = "/local/host/provision/changed";
pub const LOCAL_TARGETS: &[&str] = &[
PROVISION_START_TARGET,
PROVISION_CODE_TARGET,
PROVISION_WIFI_TARGET,
PROVISION_WIFI_SCAN_TARGET,
PROVISION_STATUS_TARGET,
];
pub const EVENT_TARGETS: &[&str] = &[PROVISION_CHANGED_EVENT];
pub const APP_CAPABILITY: &str = "host";
pub use crate::pod::{APP_INFO_LABEL, APP_INFO_LOCKED, APP_INFO_LOCKED_FOR_MS};
pub const MAX_ATTRIBUTE_BYTES: usize = 512;
pub const SCAN_TIMEOUT_MS: i64 = 12_000;
pub const HOST_INFO_ENDPOINT: &str = "host-info";
pub const HOST_ENDPOINT_UUIDS: &[(&str, u16)] = &[(HOST_INFO_ENDPOINT, 0xFF54)];
pub const FINISH_GRACE_MS: i64 = 30_000;
pub const CODE_LOCKOUT_MS: i64 = 30_000;
pub const CODE_LOCKOUT_MAX_MS: i64 = 600_000;
pub const CLAIM_TOKEN_LEN: usize = 43;
pub fn is_claim_token(token: &str) -> bool {
token.len() == CLAIM_TOKEN_LEN
&& token
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
}
pub use crate::pod::{
CommissionCodeRequest as ProvisionCodeRequest,
CommissionSessionRequest as ProvisionSessionRequest,
CommissionStartRequest as ProvisionStartRequest, CommissionWifiRequest as ProvisionWifiRequest,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProvisionState {
Connecting,
AwaitingCode,
Securing,
ReadingInfo,
AwaitingWifi,
JoiningWifi,
Completed,
Failed,
TimedOut,
}
impl ProvisionState {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::TimedOut)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HostDeviceInfo {
pub host_id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub product_id: Option<String>,
pub firmware_version: String,
pub fingerprint: String,
pub wifi_configured: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProvisionSession {
pub session_id: String,
pub revision: u64,
pub state: ProvisionState,
pub candidate: PodCandidate,
pub expires_at_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host: Option<HostDeviceInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub networks: Vec<WifiNetwork>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claim_token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<CommissionError>,
}
impl ProvisionSession {
pub fn validate(&self) -> Result<(), &'static str> {
match (self.state, self.error.as_ref().map(|error| error.code)) {
(ProvisionState::Failed | ProvisionState::TimedOut, None) => {
return Err("failed and timed_out sessions carry an error");
}
(ProvisionState::Failed | ProvisionState::TimedOut, Some(_))
| (_, None)
| (ProvisionState::AwaitingCode, Some(CommissionErrorCode::CodeRejected))
| (
ProvisionState::AwaitingWifi,
Some(
CommissionErrorCode::WifiAuthFailed
| CommissionErrorCode::WifiNotFound
| CommissionErrorCode::WifiFailed,
),
) => {}
_ => {
return Err(
"only failed, timed_out and retryable code or Wi-Fi states carry an error",
);
}
}
if let Some(code) = self.error.as_ref().map(|error| error.code) {
if matches!(
code,
CommissionErrorCode::IssueFailed
| CommissionErrorCode::DeliveryFailed
| CommissionErrorCode::ActivationFailed
) {
return Err("credential error codes do not apply to Host provisioning");
}
}
if self.state == ProvisionState::Completed && self.host.is_none() {
return Err("completed sessions carry the Host information");
}
match self.claim_token.as_deref() {
Some(_) if self.state != ProvisionState::Completed => {
return Err("only completed sessions carry a claim token");
}
Some(token) if !is_claim_token(token) => {
return Err("claim tokens are 43 base64url characters");
}
_ => {}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ProvisionStatus {
pub session: Option<ProvisionSession>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct HostFinishResponse {
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claim_token: Option<String>,
}