v0.15.7
This commit is contained in:
+30
-10
@@ -2,13 +2,18 @@ fn normalize_local_discovery_mac(value: &str) -> String {
|
||||
value.replace([':', '-'], "").trim().to_ascii_uppercase()
|
||||
}
|
||||
|
||||
fn local_discovery_candidate(device: &Device, already_added: bool) -> LocalDiscoveryCandidate {
|
||||
fn local_discovery_candidate(
|
||||
device: &Device,
|
||||
already_added: bool,
|
||||
protocol_locked: bool,
|
||||
) -> LocalDiscoveryCandidate {
|
||||
LocalDiscoveryCandidate {
|
||||
name: device.name.clone(),
|
||||
mac: device.mac.clone(),
|
||||
ip: device.ip.clone(),
|
||||
port: device.port,
|
||||
protocol_version: device.protocol_version,
|
||||
protocol_locked,
|
||||
model: device.model.clone(),
|
||||
firmware: device.firmware.clone(),
|
||||
already_added,
|
||||
@@ -24,11 +29,16 @@ fn device_from_local_discovery(candidate: LocalDiscoveryCandidate) -> Result<Dev
|
||||
.ip
|
||||
.parse::<IpAddr>()
|
||||
.map_err(|_| AppError::BadRequest(format!("invalid IP address for {mac}")))?;
|
||||
if !matches!(candidate.protocol_version, 1 | 2) {
|
||||
if candidate.protocol_version > 2 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"invalid protocol version for {mac}"
|
||||
)));
|
||||
}
|
||||
if candidate.protocol_locked && candidate.protocol_version == 0 {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"locked discovery protocol is missing for {mac}"
|
||||
)));
|
||||
}
|
||||
|
||||
let model = candidate.model.trim().to_string();
|
||||
let fallback_model = if model.is_empty() { "GREE" } else { &model };
|
||||
@@ -151,7 +161,11 @@ async fn scan_discovery(
|
||||
for device in discovered {
|
||||
let mac = normalize_local_discovery_mac(&device.mac);
|
||||
let already_added = state.db.get_device_by_mac(&mac)?.is_some();
|
||||
candidates.push(local_discovery_candidate(&device, already_added));
|
||||
candidates.push(local_discovery_candidate(
|
||||
&device,
|
||||
already_added,
|
||||
protocol_version != 0,
|
||||
));
|
||||
}
|
||||
|
||||
state.log(
|
||||
@@ -188,6 +202,7 @@ async fn add_discovered_devices(
|
||||
let mut skipped = Vec::new();
|
||||
|
||||
for candidate in request.devices {
|
||||
let protocol_locked = candidate.protocol_locked;
|
||||
let mut device = device_from_local_discovery(candidate)?;
|
||||
let _device_guard = state.lock_device_operation(&device.id).await;
|
||||
if state.db.get_device_by_mac(&device.mac)?.is_some() {
|
||||
@@ -195,13 +210,17 @@ async fn add_discovered_devices(
|
||||
continue;
|
||||
}
|
||||
|
||||
match state
|
||||
.providers
|
||||
.local()
|
||||
.client()
|
||||
.bind_exact(&device, device.protocol_version)
|
||||
.await
|
||||
{
|
||||
let client = state.providers.local().client();
|
||||
let bind_result = if protocol_locked {
|
||||
client.bind_exact(&device, device.protocol_version).await
|
||||
} else {
|
||||
// Auto discovery only provides a protocol hint. Try that generation
|
||||
// first, fall back to the other one, and persist the protocol that
|
||||
// actually completes binding.
|
||||
client.bind(&device).await
|
||||
};
|
||||
|
||||
match bind_result {
|
||||
Ok(bound) => {
|
||||
device.key = Some(bound.key);
|
||||
device.protocol_version = bound.protocol_version;
|
||||
@@ -217,6 +236,7 @@ async fn add_discovered_devices(
|
||||
json!({
|
||||
"device_id": device.id,
|
||||
"protocol_version": device.protocol_version,
|
||||
"protocol_locked": protocol_locked,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,8 +124,13 @@ pub struct LocalDiscoveryCandidate {
|
||||
pub ip: String,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
/// Detected protocol: 1 = AES-ECB, 2 = AES-GCM.
|
||||
/// Discovery protocol hint: 0 = unknown/auto, 1 = AES-ECB, 2 = AES-GCM.
|
||||
pub protocol_version: u8,
|
||||
/// True only when the discovery request explicitly selected V1 or V2.
|
||||
/// Auto discovery keeps this false so binding can verify/fallback and store
|
||||
/// the protocol that actually succeeds.
|
||||
#[serde(default)]
|
||||
pub protocol_locked: bool,
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -4,7 +4,7 @@ impl GreeClient {
|
||||
self.bind_with_versions(device, versions).await
|
||||
}
|
||||
|
||||
/// Bind using exactly the protocol detected during discovery.
|
||||
/// Bind using exactly the requested protocol. Used by explicit V1/V2 discovery.
|
||||
pub async fn bind_exact(&self, device: &Device, protocol_version: u8) -> Result<BindResult> {
|
||||
let versions = Self::exact_bind_versions(protocol_version)?;
|
||||
self.bind_with_versions(device, versions).await
|
||||
|
||||
@@ -74,40 +74,65 @@ impl GreeClient {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn discovery_protocol_hint(value: &Value, envelope_protocol: u8) -> u8 {
|
||||
// A GCM envelope proves V2 support. A V1/plain discovery envelope does
|
||||
// not prove V1 because newer modules also advertise themselves that way.
|
||||
if envelope_protocol == 2 {
|
||||
return 2;
|
||||
}
|
||||
|
||||
let ver = value
|
||||
.get("ver")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim();
|
||||
let ver = ver
|
||||
.strip_prefix('V')
|
||||
.or_else(|| ver.strip_prefix('v'))
|
||||
.unwrap_or(ver);
|
||||
let major = ver
|
||||
.split('.')
|
||||
.next()
|
||||
.and_then(|part| part.parse::<u16>().ok());
|
||||
|
||||
match major {
|
||||
Some(1) => 1,
|
||||
Some(value) if value >= 2 => 2,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_discovery(
|
||||
&self,
|
||||
mut value: Value,
|
||||
source: SocketAddr,
|
||||
protocol_filter: u8,
|
||||
) -> Result<Option<Device>> {
|
||||
let mut detected_protocol = 1_u8;
|
||||
// Discovery transport is not the same thing as the protocol used for
|
||||
// bind/status/commands. In particular, AES-GCM capable modules can
|
||||
// answer the common scan packet using a legacy/plain V1-style envelope.
|
||||
// Keep the envelope only as a hint and resolve the device generation
|
||||
// from the inner discovery payload after it has been decoded.
|
||||
let mut envelope_protocol = 0_u8;
|
||||
if value.get("t").and_then(Value::as_str) == Some("pack") {
|
||||
if let Some(pack_value) = value.get("pack") {
|
||||
if let Some(pack) = pack_value.as_str() {
|
||||
let packet_protocol = if value.get("tag").and_then(Value::as_str).is_some() {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
if protocol_filter != 0 && packet_protocol != protocol_filter {
|
||||
return Ok(None);
|
||||
}
|
||||
let clear = if let Some(tag) = value.get("tag").and_then(Value::as_str) {
|
||||
detected_protocol = 2;
|
||||
envelope_protocol = 2;
|
||||
decrypt_v2(GENERIC_GREE_V2_KEY, pack, tag)?
|
||||
} else {
|
||||
envelope_protocol = 1;
|
||||
decrypt_v1(GENERIC_GREE_V1_KEY, pack)?
|
||||
};
|
||||
value = serde_json::from_slice::<Value>(&clear)
|
||||
.context("invalid decrypted discovery JSON")?;
|
||||
} else if pack_value.is_object() {
|
||||
if protocol_filter == 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
value = pack_value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let detected_protocol = Self::discovery_protocol_hint(&value, envelope_protocol);
|
||||
if protocol_filter != 0 && detected_protocol != protocol_filter {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_bind_uses_detected_protocol_exactly() {
|
||||
fn bind_version_selection_supports_exact_and_auto_fallback() {
|
||||
assert_eq!(GreeClient::exact_bind_versions(1).unwrap(), &[1]);
|
||||
assert_eq!(GreeClient::exact_bind_versions(2).unwrap(), &[2]);
|
||||
assert!(GreeClient::exact_bind_versions(0).is_err());
|
||||
@@ -130,7 +130,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_protocol_filter_rejects_other_envelope_before_decode() {
|
||||
fn discovery_uses_inner_version_as_protocol_hint() {
|
||||
let client = GreeClient::new(
|
||||
"test-controller".into(),
|
||||
None,
|
||||
@@ -138,20 +138,78 @@ mod tests {
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
);
|
||||
let source: SocketAddr = "192.0.2.20:7000".parse().unwrap();
|
||||
|
||||
let v1 = json!({
|
||||
"t": "pack",
|
||||
"pack": {
|
||||
"t": "dev",
|
||||
"mac": "502CC699C117",
|
||||
"name": "Legacy",
|
||||
"ver": "V1.2.1"
|
||||
}
|
||||
});
|
||||
let v2_inner = json!({
|
||||
"t": "dev",
|
||||
"mac": "9424B80C5AB9",
|
||||
"name": "Modern",
|
||||
"ver": "V3.4"
|
||||
});
|
||||
let v2 = json!({
|
||||
"t": "pack",
|
||||
// Reproduce the mixed-generation case: the device is V2-capable,
|
||||
// but its discovery payload is carried in the legacy V1 envelope.
|
||||
"pack": encrypt_v1(
|
||||
GENERIC_GREE_V1_KEY,
|
||||
&serde_json::to_vec(&v2_inner).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
let legacy = client
|
||||
.parse_discovery(v1.clone(), source, 0)
|
||||
.unwrap()
|
||||
.expect("V1 discovery");
|
||||
let modern = client
|
||||
.parse_discovery(v2.clone(), source, 0)
|
||||
.unwrap()
|
||||
.expect("V2 discovery");
|
||||
|
||||
assert_eq!(legacy.protocol_version, 1);
|
||||
assert_eq!(modern.protocol_version, 2);
|
||||
assert!(client.parse_discovery(v1.clone(), source, 1).unwrap().is_some());
|
||||
assert!(client.parse_discovery(v1, source, 2).unwrap().is_none());
|
||||
assert!(client.parse_discovery(v2.clone(), source, 2).unwrap().is_some());
|
||||
assert!(client.parse_discovery(v2, source, 1).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_does_not_treat_plain_or_v1_envelope_as_definitive_v1() {
|
||||
let client = GreeClient::new(
|
||||
"test-controller".into(),
|
||||
None,
|
||||
None,
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
);
|
||||
let source: SocketAddr = "192.0.2.21:7000".parse().unwrap();
|
||||
let unknown = json!({
|
||||
"t": "pack",
|
||||
"pack": {
|
||||
"t": "dev",
|
||||
"mac": "AABBCCDDEEFF",
|
||||
"name": "Test"
|
||||
"name": "Unknown"
|
||||
}
|
||||
});
|
||||
|
||||
assert!(client
|
||||
.parse_discovery(v1.clone(), source, 1)
|
||||
let candidate = client
|
||||
.parse_discovery(unknown.clone(), source, 0)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
assert!(client.parse_discovery(v1, source, 2).unwrap().is_none());
|
||||
.expect("auto discovery must retain unknown protocol");
|
||||
assert_eq!(candidate.protocol_version, 0);
|
||||
assert!(client
|
||||
.parse_discovery(unknown.clone(), source, 1)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(client.parse_discovery(unknown, source, 2).unwrap().is_none());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user