From d3168e46690432782d360969d4b5aa62f39b7232 Mon Sep 17 00:00:00 2001 From: Matt Stavola Date: Fri, 17 Apr 2026 21:17:00 -0400 Subject: [PATCH] Add mlf-dns-route53 official plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second official DNS plugin, proving the subprocess protocol is provider-agnostic. Wraps aws-sdk-route53 for the four ops — zone lookup by DNS name (walks parent domains and hits list_hosted_zones_by_name), list_txt, upsert_txt, delete_txt. Route 53 lacks per-record IDs so (name, type) is the identity; upserts use a single UPSERT ChangeResourceRecordSets. Credential schema: access_key + secret_key required, session_token (for STS) and region (default us-east-1) optional. TXT values are quoted + escape-sequence'd on the wire; a hand-rolled unquote handles the trailing \" edge case that a naive trim_matches would corrupt. --- Cargo.lock | 768 +++++++++++++++++++++++- Cargo.toml | 1 + dns-plugins/mlf-dns-route53/Cargo.toml | 20 + dns-plugins/mlf-dns-route53/src/api.rs | 283 +++++++++ dns-plugins/mlf-dns-route53/src/main.rs | 304 ++++++++++ 5 files changed, 1346 insertions(+), 30 deletions(-) create mode 100644 dns-plugins/mlf-dns-route53/Cargo.toml create mode 100644 dns-plugins/mlf-dns-route53/src/api.rs create mode 100644 dns-plugins/mlf-dns-route53/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1c13903..104e5bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,395 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.3.1", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-route53" +version = "1.107.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edbdedafe825712933d180cfe6b1cedeb37543371a4b6da8f1c0f48d40ac82e" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.94.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699da1961a289b23842d88fe2984c6ff68735fdf9bdcbc69ceaeb2491c9bf434" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e3a4cb3b124833eafea9afd1a6cc5f8ddf3efefffc6651ef76a03cbc6b4981" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.98.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c4f19655ab0856375e169865c91264de965bd74c407c7f1e403184b1049409" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "percent-encoding", + "sha2 0.10.9", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.12", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.7.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.32", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -230,6 +619,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -347,6 +746,16 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "camino" version = "1.2.2" @@ -369,6 +778,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -482,6 +893,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -534,6 +954,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -681,6 +1111,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common 0.1.6", + "subtle", ] [[package]] @@ -725,6 +1156,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -827,6 +1270,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -961,6 +1410,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.12" @@ -972,7 +1440,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -1014,6 +1482,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex_fmt" version = "0.3.0" @@ -1065,6 +1539,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.3.1" @@ -1076,6 +1570,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1083,7 +1588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -1094,8 +1599,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1120,6 +1625,30 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.7.0" @@ -1130,9 +1659,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1143,19 +1672,35 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", - "hyper", + "http 1.3.1", + "hyper 1.7.0", "hyper-util", - "rustls", + "rustls 0.23.32", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", ] @@ -1167,7 +1712,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -1186,9 +1731,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -1457,6 +2002,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -1778,6 +2333,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "mlf-dns-route53" +version = "0.1.0" +dependencies = [ + "aws-config", + "aws-credential-types", + "aws-sdk-route53", + "mlf-plugin-host", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "mlf-integration-tests" version = "0.1.0" @@ -1974,10 +2543,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2025,6 +2594,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2097,6 +2675,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.109" @@ -2115,6 +2699,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owo-colors" version = "4.2.3" @@ -2348,6 +2938,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.6" @@ -2364,12 +2960,12 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.7.0", + "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", "js-sys", @@ -2429,6 +3025,15 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" @@ -2442,19 +3047,44 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ + "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -2464,12 +3094,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2511,6 +3152,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2518,7 +3169,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2534,6 +3198,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -2867,7 +3537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3059,13 +3729,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.32", "tokio", ] @@ -3191,8 +3871,8 @@ dependencies = [ "bitflags 2.9.4", "bytes", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower 0.5.2", @@ -3402,6 +4082,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3414,6 +4100,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3432,6 +4128,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -3959,9 +4661,9 @@ dependencies = [ "base64", "deadpool", "futures", - "http", + "http 1.3.1", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-util", "log", "once_cell", @@ -3984,6 +4686,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index cbb7166..5d2265f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "dns-plugins/mlf-dns-cloudflare", "mlf-atproto", "mlf-cli", + "dns-plugins/mlf-dns-route53", "mlf-plugin-host", "mlf-publish", "mlf-codegen", diff --git a/dns-plugins/mlf-dns-route53/Cargo.toml b/dns-plugins/mlf-dns-route53/Cargo.toml new file mode 100644 index 0000000..c57602c --- /dev/null +++ b/dns-plugins/mlf-dns-route53/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mlf-dns-route53" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "Official MLF DNS provider plugin for AWS Route 53" + +[[bin]] +name = "mlf-dns-route53" +path = "src/main.rs" + +[dependencies] +mlf-plugin-host = { path = "../../mlf-plugin-host" } +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-credential-types = "1" +aws-sdk-route53 = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tokio = { version = "1", features = ["io-util", "macros", "rt"] } diff --git a/dns-plugins/mlf-dns-route53/src/api.rs b/dns-plugins/mlf-dns-route53/src/api.rs new file mode 100644 index 0000000..a4d387f --- /dev/null +++ b/dns-plugins/mlf-dns-route53/src/api.rs @@ -0,0 +1,283 @@ +//! Thin Route 53 client wrapping `aws-sdk-route53` for the plugin's +//! four ops: zone lookup by DNS name, TXT list/upsert/delete. +//! +//! Route 53 doesn't have per-record IDs, so the plugin uses the +//! record's (name, type) pair as the identity and treats "upsert" as +//! a single UPSERT change. + +use crate::Credentials; +use aws_credential_types::Credentials as AwsCredentials; +use aws_sdk_route53::Client; +use aws_sdk_route53::config::Region; +use aws_sdk_route53::types::{ + Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet, RrType, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Route53Error { + #[error("Route 53 error: {0}")] + Api(String), +} + +pub struct Route53Client { + client: Client, +} + +impl Route53Client { + pub async fn new(creds: &Credentials) -> Self { + let region = Region::new(creds.region.clone()); + let aws_creds = AwsCredentials::new( + creds.access_key.clone(), + creds.secret_key.clone(), + creds.session_token.clone(), + None, + "mlf-dns-route53", + ); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region) + .credentials_provider(aws_creds) + .load() + .await; + Self { + client: Client::new(&config), + } + } + + /// Sanity-check the credentials by listing hosted zones (a cheap, + /// permission-scoped call). Returns a short human-friendly label — + /// AWS account alias would be nicer but needs an extra API call. + pub async fn verify(&self) -> Result { + let resp = self + .client + .list_hosted_zones() + .max_items(1) + .send() + .await + .map_err(|e| Route53Error::Api(format!("{e}")))?; + let count = resp.hosted_zones.len(); + Ok(format!("route53 ({count} zone(s) visible)")) + } + + pub async fn find_zone_for(&self, name: &str) -> Result, Route53Error> { + let stripped = name.strip_prefix("_lexicon.").unwrap_or(name); + for candidate in parent_domains(stripped) { + // `list_hosted_zones_by_name` returns zones at or after the + // name in lexicographic order, so the first match (if any) + // is our best candidate. + let resp = self + .client + .list_hosted_zones_by_name() + .dns_name(&candidate) + .max_items(1) + .send() + .await + .map_err(|e| Route53Error::Api(format!("{e}")))?; + for zone in resp.hosted_zones { + // AWS returns the zone name with a trailing dot. + let zone_name = zone.name.trim_end_matches('.'); + if zone_name == candidate { + return Ok(Some(Zone { + id: zone.id.trim_start_matches("/hostedzone/").to_string(), + name: zone_name.to_string(), + })); + } + } + } + Ok(None) + } + + pub async fn list_txt( + &self, + zone_id: &str, + name: &str, + ) -> Result, Route53Error> { + let name_with_dot = if name.ends_with('.') { + name.to_string() + } else { + format!("{name}.") + }; + let resp = self + .client + .list_resource_record_sets() + .hosted_zone_id(zone_id) + .start_record_name(&name_with_dot) + .start_record_type(RrType::Txt) + .max_items(1) + .send() + .await + .map_err(|e| Route53Error::Api(format!("{e}")))?; + let mut out = Vec::new(); + for rrset in resp.resource_record_sets { + if rrset.r#type != RrType::Txt { + continue; + } + if rrset.name.trim_end_matches('.') != name.trim_end_matches('.') { + continue; + } + // Route 53 has no per-record id; (name, type) is the identity. + // We surface the name as the `id` so the host can round-trip it. + for rr in rrset.resource_records.unwrap_or_default() { + out.push(TxtRecord { + id: rrset.name.clone(), + value: unquote(&rr.value), + }); + } + } + Ok(out) + } + + /// Create or update the TXT at `name` to hold a single value. + pub async fn upsert_txt( + &self, + zone_id: &str, + name: &str, + value: &str, + ttl: u32, + ) -> Result { + let quoted = format!("\"{}\"", escape_for_txt(value)); + let rr = ResourceRecord::builder() + .value("ed) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + let rrset = ResourceRecordSet::builder() + .name(name) + .r#type(RrType::Txt) + .ttl(ttl as i64) + .resource_records(rr) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + let change = Change::builder() + .action(ChangeAction::Upsert) + .resource_record_set(rrset) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + let batch = ChangeBatch::builder() + .changes(change) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + self.client + .change_resource_record_sets() + .hosted_zone_id(zone_id) + .change_batch(batch) + .send() + .await + .map_err(|e| Route53Error::Api(format!("{e}")))?; + Ok(name.to_string()) + } + + pub async fn delete_txt(&self, zone_id: &str, name: &str) -> Result<(), Route53Error> { + // Need current record values for the delete; skip cleanly if absent. + let existing = self.list_txt(zone_id, name).await?; + if existing.is_empty() { + return Ok(()); + } + let mut rr_builder = ResourceRecordSet::builder() + .name(name) + .r#type(RrType::Txt) + .ttl(300); + for rr in &existing { + let quoted = format!("\"{}\"", escape_for_txt(&rr.value)); + rr_builder = rr_builder.resource_records( + ResourceRecord::builder() + .value("ed) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?, + ); + } + let rrset = rr_builder + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + let change = Change::builder() + .action(ChangeAction::Delete) + .resource_record_set(rrset) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + let batch = ChangeBatch::builder() + .changes(change) + .build() + .map_err(|e| Route53Error::Api(e.to_string()))?; + self.client + .change_resource_record_sets() + .hosted_zone_id(zone_id) + .change_batch(batch) + .send() + .await + .map_err(|e| Route53Error::Api(format!("{e}")))?; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct Zone { + pub id: String, + #[allow(dead_code)] + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct TxtRecord { + pub id: String, + pub value: String, +} + +fn parent_domains(name: &str) -> Vec { + let parts: Vec<&str> = name.split('.').collect(); + (0..parts.len()).map(|i| parts[i..].join(".")).collect() +} + +fn escape_for_txt(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn unquote(s: &str) -> String { + // Route 53 surrounds TXT record values in double quotes and escapes + // internal quotes/backslashes. Strip exactly one set of outer quotes + // — using `trim_matches` would eat part of an escape sequence like + // `\"` at the end of the string. + let inner = s + .strip_prefix('"') + .and_then(|t| t.strip_suffix('"')) + .unwrap_or(s); + // Unescape: walk once so `\\"` → `\"` stays quoted, not unescaped twice. + let mut out = String::with_capacity(inner.len()); + let mut chars = inner.chars(); + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some('\\') => out.push('\\'), + Some('"') => out.push('"'), + Some(other) => { + out.push('\\'); + out.push(other); + } + None => out.push('\\'), + } + } else { + out.push(c); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parent_domains_walks_up() { + assert_eq!( + parent_domains("forum.example.com"), + vec!["forum.example.com", "example.com", "com"] + ); + } + + #[test] + fn txt_escaping_round_trips() { + let raw = r#"did=did:plc:"hello""#; + let escaped = escape_for_txt(raw); + assert!(escaped.contains("\\\"")); + let roundtrip = unquote(&format!("\"{escaped}\"")); + assert_eq!(roundtrip, raw); + } +} diff --git a/dns-plugins/mlf-dns-route53/src/main.rs b/dns-plugins/mlf-dns-route53/src/main.rs new file mode 100644 index 0000000..e9f79bd --- /dev/null +++ b/dns-plugins/mlf-dns-route53/src/main.rs @@ -0,0 +1,304 @@ +//! Official MLF DNS provider plugin for AWS Route 53. +//! +//! Options schema: +//! - `access_key` (secret, required) — AWS access key ID +//! - `secret_key` (secret, required) — AWS secret access key +//! - `region` (non-secret, optional, default `us-east-1`) — Route 53 +//! is a global service but an SDK region is required for signing. +//! - `session_token` (secret, optional) — for STS-issued temporary +//! credentials. +//! +//! Each op walks parent domains to find the matching hosted zone, +//! then performs the corresponding ChangeResourceRecordSets or +//! ListResourceRecordSets call. + +mod api; + +use api::{Route53Client, Route53Error}; +use mlf_plugin_host::plugin::{Server, empty_data, params_as}; +use mlf_plugin_host::protocol::{HelloData, OptionField, PROTOCOL_VERSION, Request}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> std::io::Result<()> { + let mut server = Server::stdio(); + + let identity = HelloData { + name: "route53".into(), + protocol_version: PROTOCOL_VERSION, + kind: Some("dns".into()), + capabilities: vec![ + "login".into(), + "list_txt".into(), + "upsert_txt".into(), + "delete_txt".into(), + "resolve_zone".into(), + ], + options_schema: vec![ + OptionField { + name: "access_key".into(), + label: "AWS access key ID".into(), + help: Some( + "An IAM access key with route53:ListHostedZonesByName, \ + route53:ListResourceRecordSets, and \ + route53:ChangeResourceRecordSets on the relevant zones." + .into(), + ), + secret: true, + required: true, + default: None, + }, + OptionField { + name: "secret_key".into(), + label: "AWS secret access key".into(), + help: None, + secret: true, + required: true, + default: None, + }, + OptionField { + name: "session_token".into(), + label: "AWS session token (for STS / temporary credentials)".into(), + help: None, + secret: true, + required: false, + default: None, + }, + OptionField { + name: "region".into(), + label: "AWS region".into(), + help: Some( + "Route 53 is global, but the SDK needs a region for signing. \ + `us-east-1` is a safe default." + .into(), + ), + secret: false, + required: false, + default: Some(Value::String("us-east-1".into())), + }, + ], + }; + + if server.handshake(identity).await.is_err() { + return Ok(()); + } + + let mut creds: Option = None; + + while let Ok(Some(req)) = server.next_request().await { + if let Err(e) = dispatch(&mut server, &req, &mut creds).await { + let _ = server.reply_err("internal", &e.to_string(), false).await; + } + } + + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Credentials { + access_key: String, + secret_key: String, + #[serde(default)] + session_token: Option, + #[serde(default = "default_region")] + region: String, +} + +fn default_region() -> String { + "us-east-1".to_string() +} + +#[derive(Debug, Deserialize)] +struct InitParams { + #[serde(default)] + credentials: Option, +} + +#[derive(Debug, Deserialize)] +struct ResolveZoneParams { + domain: String, +} + +#[derive(Debug, Deserialize)] +struct ListTxtParams { + name: String, +} + +#[derive(Debug, Deserialize)] +struct UpsertTxtParams { + name: String, + value: String, + #[serde(default)] + ttl: Option, +} + +#[derive(Debug, Deserialize)] +struct DeleteTxtParams { + name: String, + #[allow(dead_code)] + record_id: String, +} + +#[derive(thiserror::Error, Debug)] +enum DispatchError { + #[error("{0}")] + Plugin(#[from] mlf_plugin_host::plugin::PluginError), + #[error("{0}")] + Route53(#[from] Route53Error), +} + +async fn dispatch( + server: &mut Server, + req: &Request, + creds: &mut Option, +) -> Result<(), DispatchError> +where + W: tokio::io::AsyncWrite + Unpin, + R: tokio::io::AsyncBufReadExt + Unpin, +{ + match req.op.as_str() { + "init" => { + let InitParams { credentials } = params_as(req)?; + *creds = credentials; + server.reply_ok(empty_data()).await?; + } + "login" => { + let Some(c) = creds.as_ref() else { + server + .reply_err( + "no_credentials", + "login called before init set credentials", + false, + ) + .await?; + return Ok(()); + }; + match Route53Client::new(c).await.verify().await { + Ok(name) => { + server + .reply_ok(json!({ + "credentials": c, + "display_name": name, + })) + .await?; + } + Err(e) => { + server + .reply_err("invalid_credentials", &e.to_string(), false) + .await?; + } + } + } + "logout" => { + *creds = None; + server.reply_ok(empty_data()).await?; + } + "resolve_zone" => { + let ResolveZoneParams { domain } = params_as(req)?; + let c = require_creds(server, creds).await?; + let client = Route53Client::new(&c).await; + match client.find_zone_for(&domain).await? { + Some(zone) => { + server + .reply_ok(json!({ + "zone_id": zone.id, + "covered": true, + })) + .await?; + } + None => { + server + .reply_ok(json!({"zone_id": Value::Null, "covered": false})) + .await?; + } + } + } + "list_txt" => { + let ListTxtParams { name } = params_as(req)?; + let c = require_creds(server, creds).await?; + let client = Route53Client::new(&c).await; + let zone = match client.find_zone_for(&name).await? { + Some(z) => z, + None => { + server + .reply_err("unknown_zone", &format!("no zone covers {name}"), false) + .await?; + return Ok(()); + } + }; + let records = client.list_txt(&zone.id, &name).await?; + server + .reply_ok(json!({ + "records": records.into_iter().map(|r| json!({ + "id": r.id, + "value": r.value, + })).collect::>(), + })) + .await?; + } + "upsert_txt" => { + let UpsertTxtParams { name, value, ttl } = params_as(req)?; + let c = require_creds(server, creds).await?; + let client = Route53Client::new(&c).await; + let zone = match client.find_zone_for(&name).await? { + Some(z) => z, + None => { + server + .reply_err("unknown_zone", &format!("no zone covers {name}"), false) + .await?; + return Ok(()); + } + }; + let id = client + .upsert_txt(&zone.id, &name, &value, ttl.unwrap_or(300)) + .await?; + server.reply_ok(json!({ "record_id": id })).await?; + } + "delete_txt" => { + let DeleteTxtParams { name, record_id: _ } = params_as(req)?; + let c = require_creds(server, creds).await?; + let client = Route53Client::new(&c).await; + let zone = match client.find_zone_for(&name).await? { + Some(z) => z, + None => { + server + .reply_err("unknown_zone", &format!("no zone covers {name}"), false) + .await?; + return Ok(()); + } + }; + client.delete_txt(&zone.id, &name).await?; + server.reply_ok(empty_data()).await?; + } + other => { + server + .reply_err("unknown_op", &format!("unsupported op `{other}`"), false) + .await?; + } + } + Ok(()) +} + +async fn require_creds( + server: &mut Server, + creds: &Option, +) -> Result +where + W: tokio::io::AsyncWrite + Unpin, + R: tokio::io::AsyncBufReadExt + Unpin, +{ + if let Some(c) = creds.clone() { + return Ok(c); + } + server + .reply_err( + "no_credentials", + "host hasn't called init with credentials yet", + false, + ) + .await?; + Err(DispatchError::Plugin( + mlf_plugin_host::plugin::PluginError::Unexpected("missing credentials".into()), + )) +} -- 2.51.2