From 82012066e86a53c4456e0e069378fb3babe2adf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jalil=20David=20Salam=C3=A9=20Messina?= Date: Fri, 31 Jan 2025 21:04:36 +0100 Subject: [PATCH 1/6] fix(webnsupdate): make IP none when query is empty This happens when one of the IPs hasn't changed (but the other has) in the FRITZ!Box DDNS client. --- src/main.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index d522390..035b4ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -414,6 +414,24 @@ fn main() -> Result<()> { .wrap_err("failed to run main loop") } +/// Serde deserialization decorator to map empty Strings to None, +/// +/// Adapted from: +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + use serde::Deserialize; + + let opt = Option::<&'de str>::deserialize(de)?; + match opt { + None | Some("") => Ok(None), + Some(s) => s.parse::().map_err(serde::de::Error::custom).map(Some), + } +} + #[derive(Debug, serde::Deserialize)] #[serde(deny_unknown_fields)] struct FritzBoxUpdateParams { @@ -422,10 +440,10 @@ struct FritzBoxUpdateParams { #[serde(default)] domain: Option, /// IPv4 address for the domain - #[serde(default)] + #[serde(default, deserialize_with = "empty_string_as_none")] ipv4: Option, /// IPv6 address for the domain - #[serde(default)] + #[serde(default, deserialize_with = "empty_string_as_none")] ipv6: Option, /// IPv6 prefix for the home network #[allow(unused)] From bf8c16f565236c43c108d2dddf548e9f66042455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jalil=20David=20Salam=C3=A9=20Messina?= Date: Fri, 31 Jan 2025 21:04:36 +0100 Subject: [PATCH 2/6] fix(tests): add case for when query has empty string This is produced by the FRITZ!Box DDNS client. --- flake-modules/tests.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake-modules/tests.nix b/flake-modules/tests.nix index 5f0feef..45fc5ff 100644 --- a/flake-modules/tests.nix +++ b/flake-modules/tests.nix @@ -237,9 +237,9 @@ if IPV4 and IPV6: update_records("127.0.0.1", domain="test", ipv4="1.2.3.4", ipv6="::1234") elif IPV4: - update_records("127.0.0.1", ipv4="1.2.3.4") + update_records("127.0.0.1", ipv4="1.2.3.4", ipv6="") elif IPV6: - update_records("[::1]", ipv6="::1234") + update_records("[::1]", ipv4="", ipv6="::1234") for domain in DYNAMIC_DOMAINS: if IPV4: From 2f970084759169e65e4d656cf1c00dcaa48f7428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jalil=20David=20Salam=C3=A9=20Messina?= Date: Fri, 31 Jan 2025 21:04:36 +0100 Subject: [PATCH 3/6] fix(webnsupdate): make IP none when query is empty This happens when one of the IPs hasn't changed (but the other has) in the FRITZ!Box DDNS client. --- src/main.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index d522390..0a21937 100644 --- a/src/main.rs +++ b/src/main.rs @@ -414,6 +414,24 @@ fn main() -> Result<()> { .wrap_err("failed to run main loop") } +/// Serde deserialization decorator to map empty Strings to None, +/// +/// Adapted from: +fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + use serde::Deserialize; + + let opt = Option::>::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => s.parse::().map_err(serde::de::Error::custom).map(Some), + } +} + #[derive(Debug, serde::Deserialize)] #[serde(deny_unknown_fields)] struct FritzBoxUpdateParams { @@ -422,10 +440,10 @@ struct FritzBoxUpdateParams { #[serde(default)] domain: Option, /// IPv4 address for the domain - #[serde(default)] + #[serde(default, deserialize_with = "empty_string_as_none")] ipv4: Option, /// IPv6 address for the domain - #[serde(default)] + #[serde(default, deserialize_with = "empty_string_as_none")] ipv6: Option, /// IPv6 prefix for the home network #[allow(unused)] @@ -653,4 +671,48 @@ mod parse_query_params { ) "#); } + + #[test] + fn ipv4_and_empty_ipv6() { + let uri = http::Uri::builder() + .path_and_query("/update?ipv4=1.2.3.4&ipv6=") + .build() + .unwrap(); + let query: Query = Query::try_from_uri(&uri).unwrap(); + insta::assert_debug_snapshot!(query, @r#" + Query( + FritzBoxUpdateParams { + domain: None, + ipv4: Some( + 1.2.3.4, + ), + ipv6: None, + ipv6prefix: None, + dualstack: None, + }, + ) + "#); + } + + #[test] + fn empty_ipv4_and_ipv6() { + let uri = http::Uri::builder() + .path_and_query("/update?ipv4=&ipv6=%3A%3A1234") + .build() + .unwrap(); + let query: Query = Query::try_from_uri(&uri).unwrap(); + insta::assert_debug_snapshot!(query, @r#" + Query( + FritzBoxUpdateParams { + domain: None, + ipv4: None, + ipv6: Some( + ::1234, + ), + ipv6prefix: None, + dualstack: None, + }, + ) + "#); + } } From 70ed898f1d5b057977c9fd1ed1791ca27eac748b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jalil=20David=20Salam=C3=A9=20Messina?= Date: Fri, 31 Jan 2025 21:04:36 +0100 Subject: [PATCH 4/6] fix(tests): add case for when query has empty string This is produced by the FRITZ!Box DDNS client. --- flake-modules/tests.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake-modules/tests.nix b/flake-modules/tests.nix index 5f0feef..45fc5ff 100644 --- a/flake-modules/tests.nix +++ b/flake-modules/tests.nix @@ -237,9 +237,9 @@ if IPV4 and IPV6: update_records("127.0.0.1", domain="test", ipv4="1.2.3.4", ipv6="::1234") elif IPV4: - update_records("127.0.0.1", ipv4="1.2.3.4") + update_records("127.0.0.1", ipv4="1.2.3.4", ipv6="") elif IPV6: - update_records("[::1]", ipv6="::1234") + update_records("[::1]", ipv4="", ipv6="::1234") for domain in DYNAMIC_DOMAINS: if IPV4: From f61a91234b8edbd6c917abce847b13c312c31dd1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 31 Jan 2025 21:30:31 +0100 Subject: [PATCH 5/6] chore(deps): lock file maintenance --- Cargo.lock | 4 ++-- flake.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ac26f5..7f9c6bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,9 +185,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "shlex", ] diff --git a/flake.lock b/flake.lock index 80c565d..5314233 100644 --- a/flake.lock +++ b/flake.lock @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1738021509, - "narHash": "sha256-JNUiceGsr7cVBUQxLBF1ILCe99E0qLxsVuet6GsZUuw=", + "lastModified": 1738142207, + "narHash": "sha256-NGqpVVxNAHwIicXpgaVqJEJWeyqzoQJ9oc8lnK9+WC4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9db269672dbdbb519e0bd3ea24f01506c135e46f", + "rev": "9d3ae807ebd2981d593cddd0080856873139aa40", "type": "github" }, "original": { From 6124a4097bf330fdee66e9f8d5ff776949c7048b Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 31 Jan 2025 21:40:19 +0100 Subject: [PATCH 6/6] chore(deps): lock file maintenance --- Cargo.lock | 4 ++-- flake.lock | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ac26f5..7f9c6bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,9 +185,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "shlex", ] diff --git a/flake.lock b/flake.lock index 80c565d..5314233 100644 --- a/flake.lock +++ b/flake.lock @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1738021509, - "narHash": "sha256-JNUiceGsr7cVBUQxLBF1ILCe99E0qLxsVuet6GsZUuw=", + "lastModified": 1738142207, + "narHash": "sha256-NGqpVVxNAHwIicXpgaVqJEJWeyqzoQJ9oc8lnK9+WC4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9db269672dbdbb519e0bd3ea24f01506c135e46f", + "rev": "9d3ae807ebd2981d593cddd0080856873139aa40", "type": "github" }, "original": {