diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ebc6273 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Postgres (PlanetScale in production). Include sslmode=require for hosted databases. +DATABASE_URL=postgres://user:password@localhost:5432/hosted_dns +# Route 53 hosted zone for the apex, and the apex itself. +HOSTED_ZONE_ID=Z0000000000000000000 +APEX_DOMAIN=example.com +# Standard AWS credentials for an IAM user limited to that zone. +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 +# Optional. +PORT=8080 +MINTS_PER_HOUR=30 +# Set when behind a proxy that overwrites this header with the client IP. +CLIENT_IP_HEADER=x-real-ip +RUST_LOG=info +# ACME account for wildcard certificates (Google Trust Services, bound with EAB). +# Production directory: https://dv.acme-v02.api.pki.goog/directory +# Staging directory: https://dv.acme-v02.test-api.pki.goog/directory +ACME_DIRECTORY_URL=https://dv.acme-v02.test-api.pki.goog/directory +ACME_EAB_KID= +# base64url, as `gcloud publicca external-account-keys create` prints it. +ACME_EAB_HMAC_KEY= +# Optional comma-separated bearer keys that let POST /domains skip the per-IP limit. +MINT_KEYS=replace-with-a-long-random-key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..863f75b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --check + - run: cargo clippy --all-targets -- -D warnings + # Tests start Postgres through Docker (testcontainers); ubuntu-latest has it. + - run: cargo test diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..07d638a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "astral-tokio-tar" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18457efd137254e016bbde5e1d88df61c4e1a5ae2223746e56123bac6af2463" +dependencies = [ + "futures-core", + "libc", + "portable-atomic", + "rustc-hash", + "rustix", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4" +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-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.5.0", + "sha1", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8a9911551b4ea6ca13805ef52ed96f7d2bbb43cc3b4a14cb0776a71f33cfaa" +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.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-route53" +version = "1.125.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4412566fe23351d03d09e6f4ac7e3941d8d67cdd82b6e23c0a1e030173641ec5" +dependencies = [ + "arc-swap", + "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-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.112.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d59c47556b283bb2d20fc0b416df7d9d9a3d6f5fa0e77867167a3f2efc23b9" +dependencies = [ + "arc-swap", + "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-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eceb9f31b46ba6da21f666e456395624824a2a086caed940b13d78832fa3ee02" +dependencies = [ + "arc-swap", + "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-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.117.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9291df9ea767ab5e77d08a53f4f22daa917909a34ed7a22c1a4b82d193a423ad" +dependencies = [ + "arc-swap", + "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-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2312577f088c9fbf4206dfdb884cf1de9407b43e1a923cbed5237775116fc24b" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "percent-encoding", + "sha2 0.11.0", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639b4d8f8555f24a9be649811c3eb0b4d4616f4d61daf0c32e28873bc1ea9af1" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd25384a4e437aa8d8f339afad4b69e786b936a7cb10db668a7aaf66717b1a8" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.19", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.10", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.5", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3385d469edbe8b60cc72002784652b5efca39178192aa9cc4b44c9875c6bdc18" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d1d71f6562be974caa85442ecd90194c40fdb5df045f182a6c2e872ce95056" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591e0024cdf2a8de8711860f7e1d5cfbca26b800ce13793a9aa2f4f5918e6d95" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb0deb40a69de59f7809251cfe1ab16de135f42c49d222d1202b4d0ce19720f" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f395d93304280b64b7632fea798d177e74897fe7f063416ce627cd6fa24829" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.5.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69bb407740a197147da48238ecc94498493c9e85445732360cec180296ca45f1" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b932c8d6dc127fc980eecd78f8694ae9b9551b69a93a7def2a199c1c0033daf" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[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 = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0cd0bd35a28836d528d2b58ad499bc3c5641d59379421b1be9eeb0c2f2b912a" +dependencies = [ + "base64 0.23.1", + "blowfish", + "getrandom 0.4.3", + "subtle", + "zeroize", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bollard" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" +dependencies = [ + "async-stream", + "base64 0.22.1", + "bitflags 2.13.2", + "bollard-buildkit-proto", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.5.0", + "http-body-util", + "hyper 1.11.1", + "hyper-named-pipe", + "hyper-rustls 0.27.10", + "hyper-util", + "hyperlocal", + "log", + "num", + "pin-project-lite", + "rand 0.9.5", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tonic", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq", +] + +[[package]] +name = "bollard-stubs" +version = "1.52.1-rc.29.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" +dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "prost", + "serde", + "serde_json", + "serde_repr", + "time", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[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 = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "docker_credential" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.3", + "web-time", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[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 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[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 = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hosted-dns" +version = "0.1.0" +dependencies = [ + "aws-config", + "aws-sdk-route53", + "axum", + "base64 0.23.1", + "bcrypt", + "bytes", + "chrono", + "http-body-util", + "httpdate", + "hyper-rustls 0.27.10", + "hyper-util", + "instant-acme", + "rand 0.9.5", + "rcgen", + "rustls 0.23.45", + "serde", + "serde_json", + "sqlx", + "testcontainers-modules", + "thiserror", + "tokio", + "tower", + "tracing", + "tracing-subscriber", + "x509-parser", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.5.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" +dependencies = [ + "hex", + "hyper 1.11.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http 1.5.0", + "hyper 1.11.1", + "hyper-util", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-platform-verifier", + "tokio", + "tokio-rustls 0.26.5", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.11.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "hyper 1.11.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "instant-acme" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64 0.22.1", + "bytes", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "httpdate", + "hyper 1.11.1", + "hyper-rustls 0.27.10", + "hyper-util", + "rcgen", + "rustls 0.23.45", + "rustls-pki-types", + "serde", + "serde_json", + "thiserror", + "tokio", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.8", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.119", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "aws-lc-rs", + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.15", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.15", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +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 0.9.0", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.2", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls 0.23.45", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.2", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.8", + "rsa", + "serde", + "sha1", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.2", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera 0.8.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.8", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.119", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "testcontainers" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" +dependencies = [ + "astral-tokio-tar", + "async-trait", + "bollard", + "bytes", + "docker_credential", + "either", + "etcetera 0.11.0", + "ferroid", + "futures", + "http 1.5.0", + "itertools", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5985fde5befe4ffa77a052e035e16c2da86e8bae301baa9f9904ad3c494d357" +dependencies = [ + "testcontainers", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls 0.23.45", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2 0.6.5", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 2.14.2", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64 0.23.1", + "log", + "percent-encoding", + "rustls 0.23.45", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64 0.23.1", + "http 1.5.0", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d215021 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "hosted-dns" +version = "0.1.0" +edition = "2024" + +[dependencies] +aws-config = { version = "1.12.0", features = ["behavior-version-latest"] } +aws-sdk-route53 = "1.125.0" +axum = "0.8.9" +base64 = "0.23.1" +bcrypt = "0.19.3" +bytes = "1.12.1" +chrono = { version = "0.4.45", features = ["serde"] } +httpdate = "1.0.3" +hyper-rustls = { version = "0.27.10", default-features = false, features = ["http1", "http2", "native-tokio", "tls12", "rustls-platform-verifier", "aws-lc-rs"] } +hyper-util = { version = "0.1.21", features = ["client-legacy", "tokio", "http1", "http2"] } +instant-acme = "0.8.5" +rand = "0.9" +rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "migrate", "macros"] } +thiserror = "2.0.21" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } +x509-parser = "0.18.1" + +[dev-dependencies] +http-body-util = "0.1.5" +rcgen = "0.14.10" +testcontainers-modules = { version = "0.15.0", features = ["postgres"] } +tower = { version = "0.5.3", features = ["util"] } + +# bcrypt is unusably slow unoptimised; keep tests fast. +[profile.dev.package.bcrypt] +opt-level = 3 +[profile.dev.package.blowfish] +opt-level = 3 diff --git a/README.md b/README.md index 9bb3aba..7781c39 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ # hosted-dns — Ployz Hosted DNS service: generated Cluster Domains on Route 53. + +Rust axum service backed by Route 53 and Postgres. Configuration comes from the +environment; see `.env.example`. Migrations run at startup. + +Tests need Docker: `cargo test` starts a throwaway Postgres per test +(testcontainers) and runs the HTTP API against an in-memory fake Route 53 and +a fake ACME CA that validates DNS-01 through it. + +## API + +``` +GET /healthz anon 200 "ok" +POST /domains anon* {preferred?} → 201 {name, token} +POST /domains/{name}/rotate bearer → 200 {token} +PUT /domains/{name}/records bearer {a?, aaaa?} → 204 +POST /domains/{name}/lease bearer → 200 {name, lease_expires_at} +POST /domains/{name}/certificate bearer {csr} → 200 {certificate_chain_pem} +DELETE /domains/{name} bearer → 204, name retired forever +``` + +- Anonymous mints are limited per source IP. A `MINT_KEYS` key sent as the bearer skips the limit, and a wrong key gets 401. +- Every authorized call renews a 7-day lease. +- `PUT /records` replaces the whole apex set. It returns 422 `invalid_records` for an empty set, a non-public address, or more than 32 addresses per family. +- A name that has published no records and asked for no certificate after 24 hours is freed. Any other name is retired forever, either on `DELETE` or when its lease expires. +- Errors are `{"error": code, "message": text}`. diff --git a/migrations/0001_domains.sql b/migrations/0001_domains.sql new file mode 100644 index 0000000..2bc515e --- /dev/null +++ b/migrations/0001_domains.sql @@ -0,0 +1,12 @@ +-- One row per Cluster Domain ever granted. Released names keep their row +-- (retired_at set) so the primary key stops them being reissued. +CREATE TABLE domains ( + name TEXT PRIMARY KEY, + token_hash TEXT NOT NULL, + reserved_at TIMESTAMPTZ NOT NULL DEFAULT now(), + lease_expires_at TIMESTAMPTZ NOT NULL, + -- Set once records were published or a certificate was requested. Unused + -- reservations are freed after 24 hours; used names only ever retire. + used BOOLEAN NOT NULL DEFAULT false, + retired_at TIMESTAMPTZ +); diff --git a/migrations/0002_acme_accounts.sql b/migrations/0002_acme_accounts.sql new file mode 100644 index 0000000..2cbf8d1 --- /dev/null +++ b/migrations/0002_acme_accounts.sql @@ -0,0 +1,7 @@ +-- The ACME account for each directory. EAB keys are single use, so the +-- account key must outlive restarts. This is the CA account key, never a +-- certificate key. +CREATE TABLE acme_accounts ( + directory_url TEXT PRIMARY KEY, + credentials TEXT NOT NULL +); diff --git a/src/acme.rs b/src/acme.rs new file mode 100644 index 0000000..28de3b9 --- /dev/null +++ b/src/acme.rs @@ -0,0 +1,244 @@ +//! [`Ca`] backed by an ACME server (Google Trust Services) with an EAB-bound account. + +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; + +use axum::http::{HeaderMap, Request, StatusCode, header}; +use bytes::Bytes; +use hyper_util::client::legacy::Client as HyperClient; +use hyper_util::rt::TokioExecutor; +use instant_acme::{ + Account, AccountCredentials, AuthorizationHandle, AuthorizationStatus, BodyWrapper, + BytesResponse, ChallengeHandle, ChallengeType, ExternalAccountKey, HttpClient, Identifier, + NewAccount, NewOrder, Order, OrderStatus, RetryPolicy, +}; +use sqlx::PgPool; +use tokio::sync::OnceCell; + +use crate::cert::{Ca, CaError}; + +/// Order validation and certificate download get this long. +const ORDER_POLL_POLICY: RetryPolicy = RetryPolicy::new() + .initial_delay(Duration::from_secs(2)) + .backoff(1.5) + .timeout(Duration::from_secs(300)); +/// Used when a 429 carries no usable `Retry-After`. +const DEFAULT_BACKOFF: Duration = Duration::from_secs(3600); + +/// Where the ACME account lives and how to create it. +pub struct AcmeConfig { + /// ACME directory, e.g. Google Trust Services production or staging. + pub directory_url: String, + /// External Account Binding key id. + pub eab_kid: String, + /// Raw HMAC key bytes (the CA hands them out base64url-encoded). + pub eab_hmac: Vec, +} + +/// The production CA. +pub struct Acme { + config: AcmeConfig, + db: PgPool, + account: OnceCell, + rate_limited_until: Arc>>, +} + +impl Acme { + /// The account is created (or loaded from Postgres) on first use. + pub fn new(config: AcmeConfig, db: PgPool) -> Self { + Self { + config, + db, + account: OnceCell::new(), + rate_limited_until: Arc::default(), + } + } + + /// EAB keys are single use, so the account's credentials are kept in Postgres. + async fn account(&self) -> Result<&Account, CaError> { + self.account + .get_or_try_init(|| async { + let url = &self.config.directory_url; + let builder = Account::builder_with_http(Box::new(self.http()?)); + let stored: Option = sqlx::query_scalar( + "SELECT credentials FROM acme_accounts WHERE directory_url = $1", + ) + .bind(url) + .fetch_optional(&self.db) + .await + .map_err(ca_failure)?; + if let Some(stored) = stored { + let credentials: AccountCredentials = + serde_json::from_str(&stored).map_err(ca_failure)?; + return builder + .from_credentials(credentials) + .await + .map_err(|error| self.ca_error(error)); + } + let eab = + ExternalAccountKey::new(self.config.eab_kid.clone(), &self.config.eab_hmac); + let new_account = NewAccount { + contact: &[], + terms_of_service_agreed: true, + only_return_existing: false, + }; + let (account, credentials) = builder + .create(&new_account, url.clone(), Some(&eab)) + .await + .map_err(|error| self.ca_error(error))?; + sqlx::query( + "INSERT INTO acme_accounts (directory_url, credentials) VALUES ($1, $2)", + ) + .bind(url) + .bind(serde_json::to_string(&credentials).map_err(ca_failure)?) + .execute(&self.db) + .await + .map_err(ca_failure)?; + tracing::info!(id = account.id(), "created ACME account"); + Ok(account) + }) + .await + } + + fn http(&self) -> Result { + let https = hyper_rustls::HttpsConnectorBuilder::new() + .try_with_platform_verifier() + .map_err(ca_failure)? + .https_only() + .enable_http1() + .enable_http2() + .build(); + Ok(RetryAfterRecorder { + inner: HyperClient::builder(TokioExecutor::new()).build(https), + until: Arc::clone(&self.rate_limited_until), + }) + } + + fn ca_error(&self, error: instant_acme::Error) -> CaError { + match &error { + instant_acme::Error::Api(problem) if problem.status == Some(429) => { + let until = self + .rate_limited_until + .lock() + .expect("retry-after lock is never poisoned") + .take() + .unwrap_or_else(|| SystemTime::now() + DEFAULT_BACKOFF); + CaError::RateLimited { until } + } + _ => CaError::Failed(error.to_string()), + } + } +} + +impl Ca for Acme { + type Order = Order; + + async fn new_order(&self, names: &[String]) -> Result<(Order, Vec), CaError> { + let identifiers: Vec<_> = names.iter().cloned().map(Identifier::Dns).collect(); + let mut order = self + .account() + .await? + .new_order(&NewOrder::new(&identifiers)) + .await + .map_err(|error| self.ca_error(error))?; + let mut values = Vec::new(); + let mut authorizations = order.authorizations(); + while let Some(authorization) = authorizations.next().await { + let mut authorization = authorization.map_err(|error| self.ca_error(error))?; + match authorization.status { + AuthorizationStatus::Valid => {} + AuthorizationStatus::Pending => { + let challenge = dns01(&mut authorization)?; + values.push(challenge.key_authorization().dns_value()); + } + status => return Err(CaError::Failed(format!("authorization is {status:?}"))), + } + } + Ok((order, values)) + } + + async fn finalize(&self, mut order: Order, csr_der: &[u8]) -> Result { + let mut authorizations = order.authorizations(); + while let Some(authorization) = authorizations.next().await { + let mut authorization = authorization.map_err(|error| self.ca_error(error))?; + if authorization.status != AuthorizationStatus::Pending { + continue; + } + let mut challenge = dns01(&mut authorization)?; + challenge + .set_ready() + .await + .map_err(|error| self.ca_error(error))?; + } + let status = order + .poll_ready(&ORDER_POLL_POLICY) + .await + .map_err(|error| self.ca_error(error))?; + if status != OrderStatus::Ready { + return Err(CaError::Failed(format!( + "order is {status:?} after validation" + ))); + } + order + .finalize_csr(csr_der) + .await + .map_err(|error| self.ca_error(error))?; + order + .poll_certificate(&ORDER_POLL_POLICY) + .await + .map_err(|error| self.ca_error(error)) + } +} + +type HttpsClient = HyperClient< + hyper_rustls::HttpsConnector, + BodyWrapper, +>; + +/// instant-acme drops `Retry-After` from 429 problems, so record it on the way through. +// ponytail: one slot for all requests; two 429s racing may pair a problem with the +// other's Retry-After. Both come from the same account quota, so either is close. +struct RetryAfterRecorder { + inner: HttpsClient, + until: Arc>>, +} + +impl HttpClient for RetryAfterRecorder { + fn request( + &self, + request: Request>, + ) -> Pin> + Send>> { + let response = HttpClient::request(&self.inner, request); + let until = Arc::clone(&self.until); + Box::pin(async move { + let response = response.await?; + if response.parts.status == StatusCode::TOO_MANY_REQUESTS { + *until.lock().expect("retry-after lock is never poisoned") = + retry_after(&response.parts.headers); + } + Ok(response) + }) + } +} + +fn dns01<'a>( + authorization: &'a mut AuthorizationHandle<'a>, +) -> Result, CaError> { + authorization + .challenge(ChallengeType::Dns01) + .ok_or_else(|| CaError::Failed("CA offered no dns-01 challenge".into())) +} + +/// `Retry-After` as delay-seconds or an HTTP date. +fn retry_after(headers: &HeaderMap) -> Option { + let value = headers.get(header::RETRY_AFTER)?.to_str().ok()?.trim(); + match value.parse::() { + Ok(secs) => Some(SystemTime::now() + Duration::from_secs(secs)), + Err(_) => httpdate::parse_http_date(value).ok(), + } +} + +fn ca_failure(error: impl std::fmt::Display) -> CaError { + CaError::Failed(error.to_string()) +} diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..2661f32 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,613 @@ +//! HTTP API: mint, rotate, records, lease, release, certificate, and the reaper. + +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant, SystemTime}; + +use axum::extract::rejection::JsonRejection; +use axum::extract::{ConnectInfo, Path, State}; +use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header}; +use axum::response::{IntoResponse, Response}; +use axum::routing::{delete, get, post, put}; +use axum::{Json, Router}; +use chrono::{DateTime, TimeDelta, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{PgConnection, PgPool}; + +use crate::cert::{Ca, CaError, CertError, Issuer}; +use crate::domain::{self, ClusterDomain, Token}; +use crate::zone::{Change, RecordSet, RecordType, Zone, ZoneError}; + +const LEASE: TimeDelta = TimeDelta::days(7); +const UNUSED_RESERVATION: TimeDelta = TimeDelta::hours(24); +const MINT_ATTEMPTS: usize = 8; +const RECORD_TTL: i64 = 60; +const MAX_ADDRESSES: usize = 32; +const MINT_WINDOW: Duration = Duration::from_secs(3600); + +/// Service settings read from the environment at startup. +pub struct Config { + /// Zone apex every Cluster Domain sits under, e.g. `ployz.app`. + pub apex: String, + /// Mints allowed per source IP (IPv6: per /64) per hour. + pub mints_per_hour: u32, + /// Header holding the client IP when behind a proxy; the peer address otherwise. + pub client_ip_header: Option, + /// Bearer keys that let `POST /domains` skip the per-IP limit (hosted Cloud). + pub mint_keys: Vec, +} + +/// Shared state for every request. +pub struct AppState { + db: PgPool, + zone: Z, + issuer: Issuer, + config: Config, + mints: MintLimiter, +} + +impl AppState { + /// A path name that is not a label under the apex can not exist. + fn domain(&self, name: &str) -> Result { + ClusterDomain::parse(name, &self.config.apex).ok_or(ApiError::NotFound) + } +} + +impl AppState { + /// Wires the database, the DNS zone, the certificate authority and settings. + pub fn new(db: PgPool, zone: Z, ca: C, config: Config) -> Self { + let mints = MintLimiter { + per_hour: config.mints_per_hour, + windows: Mutex::default(), + }; + Self { + db, + zone, + issuer: Issuer::new(ca), + config, + mints, + } + } +} + +/// The service's routes, including `GET /healthz` for the platform's probe. +pub fn router(state: Arc>) -> Router { + Router::new() + .route("/healthz", get(|| async { "ok" })) + .route("/domains", post(mint::)) + .route("/domains/{name}", delete(release::)) + .route("/domains/{name}/rotate", post(rotate::)) + .route("/domains/{name}/lease", post(renew::)) + .route("/domains/{name}/records", put(put_records::)) + .route("/domains/{name}/certificate", post(certificate::)) + .with_state(state) +} + +#[derive(Deserialize)] +struct MintRequest { + preferred: Option, +} + +#[derive(Serialize)] +struct MintResponse { + name: ClusterDomain, + token: Token, +} + +async fn mint( + State(state): State>>, + ConnectInfo(peer): ConnectInfo, + headers: HeaderMap, + body: Result>, JsonRejection>, +) -> Result<(StatusCode, Json), ApiError> { + let body = body?; + if headers.contains_key(header::AUTHORIZATION) { + // A caller that presents a key never falls back to anonymous minting. + let key = bearer(&headers).ok_or(ApiError::Unauthorized)?; + let known = state + .config + .mint_keys + .iter() + .fold(false, |known, candidate| { + known | constant_time_eq(candidate, key) + }); + if !known { + return Err(ApiError::Unauthorized); + } + } else { + let client = client_ip(&headers, state.config.client_ip_header.as_ref(), peer.ip()); + state.mints.check(client)?; + } + let preferred = body.and_then(|Json(body)| body.preferred); + let token = Token::generate(); + let token_hash = token.hash().await?; + for label in domain::candidates(preferred.as_deref()).take(MINT_ATTEMPTS) { + let name = ClusterDomain::new(&label, &state.config.apex); + // Retired names keep their row, so the conflict also refuses them. + let inserted = sqlx::query( + "INSERT INTO domains (name, token_hash, lease_expires_at) VALUES ($1, $2, now() + $3) + ON CONFLICT (name) DO NOTHING", + ) + .bind(&name) + .bind(&token_hash) + .bind(LEASE) + .execute(&state.db) + .await? + .rows_affected() + == 1; + if inserted { + return Ok((StatusCode::CREATED, Json(MintResponse { name, token }))); + } + } + Err(ApiError::NamespaceExhausted) +} + +#[derive(Serialize)] +struct RotateResponse { + token: Token, +} + +async fn rotate( + State(state): State>>, + Path(name): Path, + headers: HeaderMap, +) -> Result, ApiError> { + let name = state.domain(&name)?; + let mut tx = state.db.begin().await?; + authorize_and_renew(&mut tx, &name, &headers).await?; + let token = Token::generate(); + sqlx::query("UPDATE domains SET token_hash = $2 WHERE name = $1") + .bind(&name) + .bind(token.hash().await?) + .execute(&mut *tx) + .await?; + tx.commit().await?; + Ok(Json(RotateResponse { token })) +} + +#[derive(Serialize)] +struct LeaseResponse { + name: ClusterDomain, + lease_expires_at: DateTime, +} + +async fn renew( + State(state): State>>, + Path(name): Path, + headers: HeaderMap, +) -> Result, ApiError> { + let name = state.domain(&name)?; + let mut tx = state.db.begin().await?; + let lease_expires_at = authorize_and_renew(&mut tx, &name, &headers).await?; + tx.commit().await?; + Ok(Json(LeaseResponse { + name, + lease_expires_at, + })) +} + +/// The full apex address set; an omitted family is removed. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ApexAddresses { + #[serde(default)] + a: Vec, + #[serde(default)] + aaaa: Vec, +} + +async fn put_records( + State(state): State>>, + Path(name): Path, + headers: HeaderMap, + body: Result, JsonRejection>, +) -> Result { + let name = state.domain(&name)?; + let Json(addresses) = body?; + let mut tx = state.db.begin().await?; + authorize_and_renew(&mut tx, &name, &headers).await?; + let desired_apex = [ + ( + RecordType::A, + public_addresses(addresses.a, |ip| is_public_v4(*ip))?, + ), + ( + RecordType::Aaaa, + public_addresses(addresses.aaaa, is_public_v6)?, + ), + ]; + if desired_apex.iter().all(|(_, values)| values.is_empty()) { + return Err(ApiError::InvalidRecords( + "at least one address is required".into(), + )); + } + + let current_apex = state.zone.record_sets(name.as_str()).await?; + let mut changes = Vec::new(); + for (kind, values) in desired_apex { + if !values.is_empty() { + changes.push(Change::Upsert(RecordSet { + name: name.as_str().to_owned(), + kind, + ttl: RECORD_TTL, + values, + })); + } else if let Some(stale) = current_apex.iter().find(|set| set.kind == kind) { + changes.push(Change::Delete(stale.clone())); + } + } + // Upserting an unchanged CNAME is a no-op, so no read is needed to write it "once". + changes.push(Change::Upsert(RecordSet { + name: name.wildcard(), + kind: RecordType::Cname, + ttl: RECORD_TTL, + values: vec![name.as_str().to_owned()], + })); + state.zone.apply(changes).await?; + + mark_used(&mut tx, &name).await?; + tx.commit().await?; + Ok(StatusCode::NO_CONTENT) +} + +async fn release( + State(state): State>>, + Path(name): Path, + headers: HeaderMap, +) -> Result { + let name = state.domain(&name)?; + let mut tx = state.db.begin().await?; + authorize_and_renew(&mut tx, &name, &headers).await?; + retire(&state.zone, &mut tx, &name).await?; + tx.commit().await?; + Ok(StatusCode::NO_CONTENT) +} + +#[derive(Deserialize)] +struct CertificateRequest { + csr: String, +} + +#[derive(Serialize)] +struct CertificateResponse { + certificate_chain_pem: String, +} + +// ponytail: no per-name lock; overlapping calls for one name clobber each other's +// challenge and one fails. Cloud runs one sync per Organization at a time. +async fn certificate( + State(state): State>>, + Path(name): Path, + headers: HeaderMap, + body: Result, JsonRejection>, +) -> Result, ApiError> { + let name = state.domain(&name)?; + let Json(request) = body?; + let mut tx = state.db.begin().await?; + authorize_and_renew(&mut tx, &name, &headers).await?; + // A certificate may exist from here on, so the reaper must never free this name. + mark_used(&mut tx, &name).await?; + // Issuance takes minutes; do not hold the row lock through it. + tx.commit().await?; + let certificate_chain_pem = state.issuer.issue(&state.zone, &name, &request.csr).await?; + Ok(Json(CertificateResponse { + certificate_chain_pem, + })) +} + +/// Frees reservations that never published records or asked for a certificate +/// within 24 hours, and retires names whose lease expired (removing their records). +/// +/// # Errors +/// +/// Returns [`ApiError`] on the first database or Route 53 failure; the next run retries. +pub async fn reap(state: &AppState) -> Result<(), ApiError> { + sqlx::query( + "DELETE FROM domains + WHERE retired_at IS NULL AND NOT used AND reserved_at < now() - $1", + ) + .bind(UNUSED_RESERVATION) + .execute(&state.db) + .await?; + // ponytail: one name per transaction; a name Route 53 keeps refusing stalls the rest until fixed. + loop { + let mut tx = state.db.begin().await?; + let Some(name) = sqlx::query_scalar::<_, ClusterDomain>( + "SELECT name FROM domains WHERE retired_at IS NULL AND lease_expires_at < now() + LIMIT 1 FOR UPDATE SKIP LOCKED", + ) + .fetch_optional(&mut *tx) + .await? + else { + return Ok(()); + }; + retire(&state.zone, &mut tx, &name).await?; + tx.commit().await?; + tracing::info!(%name, "retired expired lease"); + } +} + +/// Once records or a certificate exist, only the lease decides the name's fate. +async fn mark_used(tx: &mut PgConnection, name: &ClusterDomain) -> Result<(), ApiError> { + sqlx::query("UPDATE domains SET used = true WHERE name = $1") + .bind(name) + .execute(tx) + .await?; + Ok(()) +} + +/// Removes the apex and wildcard records and marks the name retired forever. +async fn retire( + zone: &Z, + tx: &mut PgConnection, + name: &ClusterDomain, +) -> Result<(), ApiError> { + let mut changes = Vec::new(); + for set_name in name.names() { + changes.extend( + zone.record_sets(&set_name) + .await? + .into_iter() + .map(Change::Delete), + ); + } + if !changes.is_empty() { + zone.apply(changes).await?; + } + sqlx::query("UPDATE domains SET retired_at = now() WHERE name = $1") + .bind(name) + .execute(tx) + .await?; + Ok(()) +} + +/// Locks the name's row for the transaction, checks the bearer token, and +/// renews the lease: every authorized call keeps the name alive. Returns the +/// new lease expiry. +async fn authorize_and_renew( + tx: &mut PgConnection, + name: &ClusterDomain, + headers: &HeaderMap, +) -> Result, ApiError> { + let token = Token::from_bearer(bearer(headers).ok_or(ApiError::Unauthorized)?); + let row: Option<(String, Option>)> = + sqlx::query_as("SELECT token_hash, retired_at FROM domains WHERE name = $1 FOR UPDATE") + .bind(name) + .fetch_optional(&mut *tx) + .await?; + let Some((token_hash, retired_at)) = row else { + return Err(ApiError::NotFound); + }; + if retired_at.is_some() { + return Err(ApiError::Retired); + } + if !token.verify(token_hash).await? { + return Err(ApiError::Unauthorized); + } + let lease_expires_at = sqlx::query_scalar( + "UPDATE domains SET lease_expires_at = now() + $2 WHERE name = $1 + RETURNING lease_expires_at", + ) + .bind(name) + .bind(LEASE) + .fetch_one(&mut *tx) + .await?; + Ok(lease_expires_at) +} + +fn bearer(headers: &HeaderMap) -> Option<&str> { + headers + .get(header::AUTHORIZATION)? + .to_str() + .ok()? + .strip_prefix("Bearer ") +} + +/// Compares without an early exit on the first differing byte. Length still +/// leaks, which says nothing useful about a random key. +fn constant_time_eq(a: &str, b: &str) -> bool { + a.len() == b.len() + && a.bytes() + .zip(b.bytes()) + .fold(0, |diff, (x, y)| diff | (x ^ y)) + == 0 +} + +/// Sorts and dedups `addresses` into record values, refusing non-public +/// addresses and sets larger than [`MAX_ADDRESSES`]. +fn public_addresses( + mut addresses: Vec, + is_public: impl Fn(&T) -> bool, +) -> Result, ApiError> { + addresses.sort(); + addresses.dedup(); + if addresses.len() > MAX_ADDRESSES { + return Err(ApiError::InvalidRecords(format!( + "at most {MAX_ADDRESSES} addresses per family" + ))); + } + if let Some(bad) = addresses.iter().find(|ip| !is_public(ip)) { + return Err(ApiError::InvalidRecords(format!( + "{} is not a public address", + bad.to_string() + ))); + } + Ok(addresses.iter().map(ToString::to_string).collect()) +} + +fn is_public_v4(ip: Ipv4Addr) -> bool { + !(ip.is_private() + || ip.is_loopback() + || ip.is_link_local() + || ip.is_unspecified() + || ip.is_multicast() + || ip.is_broadcast()) +} + +fn is_public_v6(ip: &Ipv6Addr) -> bool { + !(ip.is_loopback() + || ip.is_unspecified() + || ip.is_multicast() + || ip.is_unique_local() + || ip.is_unicast_link_local() + || ip.to_ipv4_mapped().is_some()) +} + +fn client_ip(headers: &HeaderMap, header: Option<&HeaderName>, peer: IpAddr) -> IpAddr { + header + .and_then(|header| headers.get(header)) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.trim().parse().ok()) + .unwrap_or(peer) +} + +/// Fixed-window mint counter per source address. +// ponytail: in memory and per instance; move to Postgres if the service ever runs replicas. +struct MintLimiter { + per_hour: u32, + windows: Mutex>, +} + +impl MintLimiter { + fn check(&self, ip: IpAddr) -> Result<(), ApiError> { + // One IPv6 user trivially holds a /64, so count the /64. + let key = match ip { + IpAddr::V6(v6) => IpAddr::V6(Ipv6Addr::from(u128::from(v6) & !u128::from(u64::MAX))), + IpAddr::V4(_) => ip, + }; + let now = Instant::now(); + let mut windows = self + .windows + .lock() + .expect("mint limiter lock is never poisoned"); + windows.retain(|_, (start, _)| now.duration_since(*start) < MINT_WINDOW); + let (start, count) = windows.entry(key).or_insert((now, 0)); + if *count >= self.per_hour { + let retry_after_secs = (MINT_WINDOW - now.duration_since(*start)).as_secs().max(1); + return Err(ApiError::RateLimited { retry_after_secs }); + } + *count += 1; + Ok(()) + } +} + +/// Every failure the API returns, rendered as `{"error": code, "message": text}`. +#[derive(Debug, thiserror::Error)] +pub enum ApiError { + /// Malformed JSON, bad address syntax, or unknown fields: 400. + #[error("{0}")] + BadRequest(String), + /// Empty, non-public or oversized address set: 422. + #[error("{0}")] + InvalidRecords(String), + /// Missing or wrong bearer token or mint key: 401. + #[error("missing or wrong bearer token")] + Unauthorized, + /// Never minted, or reaped: 404. + #[error("no such domain")] + NotFound, + /// Released or lease expired: 410. + #[error("domain was released and is retired")] + Retired, + /// Anonymous mint limit hit: 429 with `Retry-After`. + #[error("too many domains minted from this address")] + RateLimited { retry_after_secs: u64 }, + /// Every label attempt collided: 503. + #[error("no free label found, try again")] + NamespaceExhausted, + /// CSR is not PEM PKCS#10 naming exactly `name` and `*.name`: 422. + #[error("{0}")] + InvalidCsr(String), + /// CA rate limit (429 with `Retry-After`) or failure (502). + #[error("certificate authority: {0}")] + Ca(CaError), + /// Route 53 failed: 502. + #[error("dns provider: {0}")] + Zone(#[from] ZoneError), + /// Postgres failed: 500. + #[error("database: {0}")] + Db(#[from] sqlx::Error), + /// Token hashing failed: 500. + #[error("token hashing: {0}")] + Hash(#[from] bcrypt::BcryptError), +} + +impl From for ApiError { + fn from(error: CertError) -> Self { + match error { + CertError::InvalidCsr(why) => Self::InvalidCsr(why), + CertError::Ca(error) => Self::Ca(error), + CertError::Zone(error) => Self::Zone(error), + } + } +} + +impl From for ApiError { + fn from(rejection: JsonRejection) -> Self { + Self::BadRequest(rejection.body_text()) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let seconds_until = |until: &SystemTime| { + until + .duration_since(SystemTime::now()) + .map_or(1, |left| left.as_secs().max(1)) + }; + // AWS errors can carry account ids, so upstream DNS and internal + // failures are logged, not returned. CA problems are safe and Cloud + // needs the reason. + let (status, code, retry_after_secs, public) = match &self { + Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "invalid_request", None, true), + Self::InvalidRecords(_) => ( + StatusCode::UNPROCESSABLE_ENTITY, + "invalid_records", + None, + true, + ), + Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized", None, true), + Self::NotFound => (StatusCode::NOT_FOUND, "not_found", None, true), + Self::Retired => (StatusCode::GONE, "retired", None, true), + Self::RateLimited { retry_after_secs } => ( + StatusCode::TOO_MANY_REQUESTS, + "rate_limited", + Some(*retry_after_secs), + true, + ), + Self::NamespaceExhausted => ( + StatusCode::SERVICE_UNAVAILABLE, + "namespace_exhausted", + None, + true, + ), + Self::InvalidCsr(_) => (StatusCode::UNPROCESSABLE_ENTITY, "invalid_csr", None, true), + Self::Ca(CaError::RateLimited { until }) => ( + StatusCode::TOO_MANY_REQUESTS, + "ca_rate_limited", + Some(seconds_until(until)), + true, + ), + Self::Ca(CaError::Failed(_)) => (StatusCode::BAD_GATEWAY, "ca_error", None, true), + Self::Zone(_) => (StatusCode::BAD_GATEWAY, "dns_provider_error", None, false), + Self::Db(_) | Self::Hash(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, "internal", None, false) + } + }; + let message = if public { + self.to_string() + } else { + tracing::error!(error = %self, "request failed"); + "internal error".to_owned() + }; + let body = Json(serde_json::json!({ "error": code, "message": message })); + let mut response = (status, body).into_response(); + if let Some(secs) = retry_after_secs { + response + .headers_mut() + .insert(header::RETRY_AFTER, HeaderValue::from(secs)); + } + response + } +} diff --git a/src/cert.rs b/src/cert.rs new file mode 100644 index 0000000..63472c6 --- /dev/null +++ b/src/cert.rs @@ -0,0 +1,187 @@ +//! Wildcard certificates: CSR scope check and DNS-01 issuance through the zone. + +use std::collections::BTreeSet; +use std::sync::Mutex; +use std::time::SystemTime; + +use x509_parser::certification_request::X509CertificationRequest; +use x509_parser::extensions::{GeneralName, ParsedExtension}; +use x509_parser::prelude::FromDer; + +use crate::domain::ClusterDomain; +use crate::zone::{Change, RecordSet, RecordType, Zone, ZoneError}; + +const CHALLENGE_TTL: i64 = 60; + +/// The certificate authority seam: Google Trust Services over ACME in +/// production, a fake in tests. +pub trait Ca: Send + Sync + 'static { + /// An open order, carried from [`Ca::new_order`] to [`Ca::finalize`]. + type Order: Send; + + /// Opens an order for `names` and returns the DNS-01 TXT values the CA + /// expects at `_acme-challenge.`. Empty when every authorization is + /// already valid. + /// + /// # Errors + /// + /// Returns [`CaError`] when the CA refuses or fails the order. + fn new_order( + &self, + names: &[String], + ) -> impl Future), CaError>> + Send; + + /// Tells the CA the challenges are live, waits for validation, submits + /// `csr_der` and returns the PEM certificate chain. + /// + /// # Errors + /// + /// Returns [`CaError`] when validation, finalisation or download fails. + fn finalize( + &self, + order: Self::Order, + csr_der: &[u8], + ) -> impl Future> + Send; +} + +/// A CA failure. +#[derive(Debug, thiserror::Error)] +pub enum CaError { + /// The CA answered 429; no call may reach it before `until`. + #[error("certificate authority rate limit")] + RateLimited { until: SystemTime }, + /// Any other refusal or failure, with the CA's explanation. + #[error("{0}")] + Failed(String), +} + +/// Why a certificate request failed. +#[derive(Debug, thiserror::Error)] +pub enum CertError { + /// The CSR is not PEM PKCS#10 naming exactly `name` and `*.name`. + #[error("{0}")] + InvalidCsr(String), + /// The CA refused, failed or rate-limited the order. + #[error("certificate authority: {0}")] + Ca(#[from] CaError), + /// Route 53 failed writing, syncing or removing the challenge. + #[error("dns provider: {0}")] + Zone(#[from] ZoneError), +} + +/// Issues certificates through one CA account and owns its backoff: the CA's +/// quota is per account, so one 429 pauses every name until `Retry-After`. +pub(crate) struct Issuer { + ca: C, + blocked_until: Mutex>, +} + +impl Issuer { + pub(crate) fn new(ca: C) -> Self { + Self { + ca, + blocked_until: Mutex::default(), + } + } + + /// Checks `csr_pem`, orders `name` + `*.name`, publishes the DNS-01 TXT + /// record, waits for the zone to sync, finalises and returns the PEM chain. + /// The TXT record is removed whether or not issuance succeeds. + /// + /// # Errors + /// + /// Returns [`CertError`] for a bad CSR, an active or new CA rate limit, or + /// a CA or zone failure. + pub(crate) async fn issue( + &self, + zone: &Z, + name: &ClusterDomain, + csr_pem: &str, + ) -> Result { + let csr_der = csr_der(csr_pem, name)?; + let blocked_until = *self + .blocked_until + .lock() + .expect("backoff lock is never poisoned"); + if let Some(until) = blocked_until + && until > SystemTime::now() + { + return Err(CaError::RateLimited { until }.into()); + } + let issued = order_with_dns01(zone, &self.ca, name, &csr_der).await; + if let Err(CertError::Ca(CaError::RateLimited { until })) = &issued { + *self + .blocked_until + .lock() + .expect("backoff lock is never poisoned") = Some(*until); + } + issued + } +} + +/// Returns the DER of `pem` if it is a CSR naming exactly `name` and `*.name`. +fn csr_der(pem: &str, name: &ClusterDomain) -> Result, CertError> { + let invalid = |why: &str| CertError::InvalidCsr(why.to_owned()); + let (_, pem) = + x509_parser::pem::parse_x509_pem(pem.as_bytes()).map_err(|_| invalid("csr is not PEM"))?; + if pem.label != "CERTIFICATE REQUEST" { + return Err(invalid("PEM block is not a CERTIFICATE REQUEST")); + } + let (_, csr) = X509CertificationRequest::from_der(&pem.contents) + .map_err(|_| invalid("csr is not a valid PKCS#10 request"))?; + + let expected = BTreeSet::from(name.names()); + let mut requested = BTreeSet::new(); + for extension in csr.requested_extensions().into_iter().flatten() { + let ParsedExtension::SubjectAlternativeName(san) = extension else { + continue; + }; + for general_name in &san.general_names { + let GeneralName::DNSName(dns) = general_name else { + return Err(invalid("csr may only name DNS names")); + }; + requested.insert((*dns).to_owned()); + } + } + let common_names_ok = csr + .certification_request_info + .subject + .iter_common_name() + .all(|cn| cn.as_str().is_ok_and(|cn| expected.contains(cn))); + if requested != expected || !common_names_ok { + return Err(CertError::InvalidCsr(format!( + "csr must name exactly {name} and *.{name}" + ))); + } + Ok(pem.contents) +} + +async fn order_with_dns01( + zone: &Z, + ca: &C, + name: &ClusterDomain, + csr_der: &[u8], +) -> Result { + let (order, values) = ca.new_order(&name.names()).await?; + if values.is_empty() { + return Ok(ca.finalize(order, csr_der).await?); + } + // Both authorizations validate at the same name, so one set holds both values. + let challenge = RecordSet { + name: name.challenge_name(), + kind: RecordType::Txt, + ttl: CHALLENGE_TTL, + values, + }; + let change = zone.apply(vec![Change::Upsert(challenge.clone())]).await?; + let issued = async { + zone.wait_in_sync(&change).await?; + Ok(ca.finalize(order, csr_der).await?) + } + .await; + if let Err(error) = zone.apply(vec![Change::Delete(challenge)]).await { + // The next issuance for this name overwrites it with an upsert. + tracing::warn!(%name, %error, "could not remove DNS-01 challenge"); + } + issued +} diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..a07605a --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,161 @@ +//! Cluster Domain names, their bearer tokens, and label choice for new names. + +use bcrypt::BcryptError; +use rand::Rng; +use serde::Serialize; + +/// Labels that are never granted verbatim; they get a suffix instead. +const RESERVED: &[&str] = &[ + "www", + "api", + "dns", + "mail", + "admin", + "cloud", + "app", + "ployz", + "ns", + "ns1", + "ns2", + "smtp", + "imap", + "status", + "docs", + "dashboard", + "support", + "root", + "autoconfig", + "autodiscover", +]; + +const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; +/// Length of a bearer token: 40 characters of `[a-z0-9]`, about 206 random bits. +const TOKEN_LEN: usize = 40; +// Tokens are 200+ random bits, so bcrypt's work factor guards nothing brute force could reach. +const BCRYPT_COST: u32 = 10; +const SUFFIX_LEN: usize = 4; +const RANDOM_LABEL_LEN: usize = 10; + +/// Labels to try, best first: the preferred label if valid and not reserved, +/// then endless `preferred-xxxx` candidates, or endless random labels when the +/// preferred label is missing or invalid. +pub(crate) fn candidates(preferred: Option<&str>) -> impl Iterator + '_ { + let base = preferred.filter(|label| is_valid(label)); + let exact = base + .filter(|label| !RESERVED.contains(label)) + .map(str::to_owned); + exact + .into_iter() + .chain(std::iter::repeat_with(move || match base { + Some(base) => format!("{base}-{}", random_string(SUFFIX_LEN)), + None => random_string(RANDOM_LABEL_LEN), + })) +} + +/// A preferred label: `[a-z0-9-]{3,40}` with no hyphen at either edge. +fn is_valid(label: &str) -> bool { + (3..=40).contains(&label.len()) && is_ldh(label) +} + +/// Lowercase letters, digits and inner hyphens only. +fn is_ldh(label: &str) -> bool { + label + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') + && !label.starts_with('-') + && !label.ends_with('-') +} + +/// Random `[a-z0-9]` string. +fn random_string(len: usize) -> String { + let mut rng = rand::rng(); + (0..len) + .map(|_| char::from(ALPHABET[rng.random_range(0..ALPHABET.len())])) + .collect() +} + +/// A granted Cluster Domain, e.g. `acme.ployz.app`: one label under the apex. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, sqlx::Type)] +#[serde(transparent)] +#[sqlx(transparent)] +pub(crate) struct ClusterDomain(String); + +impl ClusterDomain { + pub(crate) fn new(label: &str, apex: &str) -> Self { + Self(format!("{label}.{apex}")) + } + + /// Accepts exactly one DNS label under `apex`. `None` for anything else, + /// which callers treat like an unknown name. + // The apex is runtime config, so this is a function rather than FromStr/serde. + pub(crate) fn parse(name: &str, apex: &str) -> Option { + let label = name.strip_suffix(apex)?.strip_suffix('.')?; + let valid = (1..=63).contains(&label.len()) && is_ldh(label); + valid.then(|| Self(name.to_owned())) + } + + pub(crate) fn as_str(&self) -> &str { + &self.0 + } + + /// `*.name`, the CNAME to the apex and the certificate's second name. + pub(crate) fn wildcard(&self) -> String { + format!("*.{}", self.0) + } + + /// `[name, *.name]`: the records' owners and the certificate's names. + pub(crate) fn names(&self) -> [String; 2] { + [self.0.clone(), self.wildcard()] + } + + /// `_acme-challenge.name`, where DNS-01 validates both `name` and `*.name`. + pub(crate) fn challenge_name(&self) -> String { + format!("_acme-challenge.{}", self.0) + } +} + +impl std::fmt::Display for ClusterDomain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +/// A per-name bearer token. Shown to the caller once; only its bcrypt hash is stored. +#[derive(Serialize)] +#[serde(transparent)] +pub(crate) struct Token(String); + +impl Token { + pub(crate) fn generate() -> Self { + Self(random_string(TOKEN_LEN)) + } + + pub(crate) fn from_bearer(token: &str) -> Self { + Self(token.to_owned()) + } + + /// # Errors + /// + /// Returns [`BcryptError`] when hashing fails. + pub(crate) async fn hash(&self) -> Result { + let token = self.0.clone(); + bcrypt_blocking(move || bcrypt::hash(token, BCRYPT_COST)).await + } + + /// # Errors + /// + /// Returns [`BcryptError`] when `hash` is not a bcrypt hash. + pub(crate) async fn verify(&self, hash: String) -> Result { + let token = self.0.clone(); + bcrypt_blocking(move || bcrypt::verify(token, &hash)).await + } +} + +/// bcrypt is CPU-bound, so it runs off the async workers. +async fn bcrypt_blocking( + work: impl FnOnce() -> Result + Send + 'static, +) -> Result { + tokio::task::spawn_blocking(work) + .await + .expect("bcrypt does not panic") +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0300486 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +//! Hosted DNS: mints generated Cluster Domains under one Route 53 zone and +//! publishes their apex addresses for Ployz Cloud. Signs one wildcard +//! certificate per name from a CSR; it never holds certificate keys. + +mod acme; +mod api; +mod cert; +mod domain; +mod route53; +mod zone; + +pub use acme::{Acme, AcmeConfig}; +pub use api::{ApiError, AppState, Config, reap, router}; +pub use cert::{Ca, CaError, CertError}; +pub use route53::Route53; +pub use zone::{Change, ChangeId, RecordSet, RecordType, Zone, ZoneError}; + +/// Schema migrations, run at startup. +pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!(); diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2facf0a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,84 @@ +//! Hosted DNS server. Configuration comes from the environment; see `.env.example`. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use hosted_dns::{Acme, AcmeConfig, AppState, Config, MIGRATOR, Route53, reap, router}; +use sqlx::PgPool; +use tracing_subscriber::EnvFilter; + +const REAP_EVERY: Duration = Duration::from_secs(300); + +#[tokio::main] +async fn main() -> Result<(), Box> { + // sqlx pulls in ring and hyper-rustls aws-lc-rs; rustls needs to be told which to use. + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .expect("no other crypto provider is installed first"); + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) + .init(); + + let db = PgPool::connect(&required("DATABASE_URL")?).await?; + MIGRATOR.run(&db).await?; + let aws = aws_config::load_from_env().await; + let zone = Route53::new(&aws, required("HOSTED_ZONE_ID")?); + let ca = Acme::new( + AcmeConfig { + directory_url: required("ACME_DIRECTORY_URL")?, + eab_kid: required("ACME_EAB_KID")?, + eab_hmac: URL_SAFE_NO_PAD + .decode(required("ACME_EAB_HMAC_KEY")?.trim_end_matches('=')) + .map_err(|_| "ACME_EAB_HMAC_KEY is not base64url")?, + }, + db.clone(), + ); + let config = Config { + apex: required("APEX_DOMAIN")?, + mints_per_hour: optional("MINTS_PER_HOUR")?.unwrap_or(30), + client_ip_header: optional("CLIENT_IP_HEADER")?, + mint_keys: std::env::var("MINT_KEYS") + .unwrap_or_default() + .split(',') + .map(str::trim) + .filter(|key| !key.is_empty()) + .map(str::to_owned) + .collect(), + }; + let state = Arc::new(AppState::new(db, zone, ca, config)); + + let reaper = Arc::clone(&state); + tokio::spawn(async move { + let mut tick = tokio::time::interval(REAP_EVERY); + loop { + tick.tick().await; + if let Err(error) = reap(&reaper).await { + tracing::error!(%error, "reap failed"); + } + } + }); + + let port: u16 = optional("PORT")?.unwrap_or(8080); + let listener = tokio::net::TcpListener::bind(("::", port)).await?; + tracing::info!(port, "listening"); + axum::serve( + listener, + router(state).into_make_service_with_connect_info::(), + ) + .await?; + Ok(()) +} + +fn required(key: &str) -> Result { + std::env::var(key).map_err(|_| format!("{key} is not set")) +} + +fn optional(key: &str) -> Result, String> { + std::env::var(key) + .ok() + .map(|value| value.parse().map_err(|_| format!("{key} is invalid"))) + .transpose() +} diff --git a/src/route53.rs b/src/route53.rs new file mode 100644 index 0000000..a48b1e8 --- /dev/null +++ b/src/route53.rs @@ -0,0 +1,156 @@ +//! [`Zone`] backed by one Route 53 hosted zone. + +use std::time::Duration; + +use aws_sdk_route53::error::DisplayErrorContext; +use aws_sdk_route53::types as r53; + +use crate::zone::{Change, ChangeId, RecordSet, RecordType, Zone, ZoneError}; + +const SYNC_POLL: Duration = Duration::from_secs(3); +const SYNC_TIMEOUT: Duration = Duration::from_secs(300); + +/// The production hosted zone. +pub struct Route53 { + client: aws_sdk_route53::Client, + zone_id: String, +} + +impl Route53 { + /// Uses credentials and region from the standard AWS environment. + pub fn new(config: &aws_config::SdkConfig, zone_id: String) -> Self { + Self { + client: aws_sdk_route53::Client::new(config), + zone_id, + } + } +} + +impl Zone for Route53 { + async fn record_sets(&self, name: &str) -> Result, ZoneError> { + // Listing starts at `name`; its own sets sort first, so one small page is enough. + let page = self + .client + .list_resource_record_sets() + .hosted_zone_id(&self.zone_id) + .start_record_name(name) + .max_items(10) + .send() + .await + .map_err(zone_error)?; + Ok(page + .resource_record_sets() + .iter() + .filter(|set| normalize(set.name()) == name) + .filter_map(|set| { + // TXT is write-only here: challenges are deleted with the values we wrote. + let kind = [RecordType::A, RecordType::Aaaa, RecordType::Cname] + .into_iter() + .find(|kind| rr_type(*kind) == *set.r#type())?; + Some(RecordSet { + name: name.to_owned(), + kind, + ttl: set.ttl().unwrap_or_default(), + values: set + .resource_records() + .iter() + .map(|record| record.value().to_owned()) + .collect(), + }) + }) + .collect()) + } + + async fn apply(&self, changes: Vec) -> Result { + let changes = changes + .into_iter() + .map(to_r53) + .collect::, _>>() + .map_err(zone_error)?; + let batch = r53::ChangeBatch::builder() + .set_changes(Some(changes)) + .build() + .map_err(zone_error)?; + let output = self + .client + .change_resource_record_sets() + .hosted_zone_id(&self.zone_id) + .change_batch(batch) + .send() + .await + .map_err(zone_error)?; + let id = output + .change_info() + .map(|info| info.id().to_owned()) + .ok_or_else(|| ZoneError("Route 53 returned no change id".into()))?; + Ok(ChangeId(id)) + } + + async fn wait_in_sync(&self, change: &ChangeId) -> Result<(), ZoneError> { + let wait = async { + loop { + let output = self + .client + .get_change() + .id(&change.0) + .send() + .await + .map_err(zone_error)?; + let in_sync = output + .change_info() + .is_some_and(|info| *info.status() == r53::ChangeStatus::Insync); + if in_sync { + return Ok(()); + } + tokio::time::sleep(SYNC_POLL).await; + } + }; + tokio::time::timeout(SYNC_TIMEOUT, wait) + .await + .map_err(|_| ZoneError(format!("change {} not in sync after 5 min", change.0)))? + } +} + +fn to_r53(change: Change) -> Result { + let (action, set) = match change { + Change::Upsert(set) => (r53::ChangeAction::Upsert, set), + Change::Delete(set) => (r53::ChangeAction::Delete, set), + }; + let records = set + .values + .iter() + .map(|value| match set.kind { + RecordType::Txt => format!("\"{value}\""), + _ => value.clone(), + }) + .map(|value| r53::ResourceRecord::builder().value(value).build()) + .collect::, _>>()?; + let set = r53::ResourceRecordSet::builder() + .r#type(rr_type(set.kind)) + .name(set.name) + .ttl(set.ttl) + .set_resource_records(Some(records)) + .build()?; + r53::Change::builder() + .action(action) + .resource_record_set(set) + .build() +} + +fn rr_type(kind: RecordType) -> r53::RrType { + match kind { + RecordType::A => r53::RrType::A, + RecordType::Aaaa => r53::RrType::Aaaa, + RecordType::Cname => r53::RrType::Cname, + RecordType::Txt => r53::RrType::Txt, + } +} + +/// Route 53 returns `\052.acme.ployz.app.` for `*.acme.ployz.app`. +fn normalize(name: &str) -> String { + name.trim_end_matches('.').replace("\\052", "*") +} + +fn zone_error(error: impl std::error::Error) -> ZoneError { + ZoneError(DisplayErrorContext(error).to_string()) +} diff --git a/src/zone.rs b/src/zone.rs new file mode 100644 index 0000000..7a28984 --- /dev/null +++ b/src/zone.rs @@ -0,0 +1,76 @@ +//! The DNS zone seam: Route 53 in production, a fake in tests. + +/// Record types the service manages. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum RecordType { + /// Apex IPv4 addresses. + A, + /// Apex IPv6 addresses. + Aaaa, + /// `*.name` pointing at the apex. + Cname, + /// DNS-01 challenge values, unquoted; the provider adds the quotes. + Txt, +} + +/// One Route 53 resource record set. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RecordSet { + /// Fully qualified name without the trailing dot, e.g. `*.acme.ployz.app`. + pub name: String, + /// The record type. + pub kind: RecordType, + /// Time to live in seconds. + pub ttl: i64, + /// Record values, e.g. addresses or the CNAME target. + pub values: Vec, +} + +/// A change with Route 53 semantics: `Delete` must match the existing set exactly. +#[derive(Clone, Debug)] +pub enum Change { + /// Create or replace the set. + Upsert(RecordSet), + /// Remove the set; it must match what the zone holds. + Delete(RecordSet), +} + +/// Identifies an applied batch so callers can wait for it to reach every nameserver. +#[derive(Clone, Debug)] +pub struct ChangeId(pub String); + +/// The DNS provider refused or failed a call. +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +pub struct ZoneError(pub String); + +/// The hosted zone that holds every Cluster Domain. +pub trait Zone: Send + Sync + 'static { + /// Lists the managed record sets whose name is exactly `name`. + /// + /// # Errors + /// + /// Returns [`ZoneError`] when the provider call fails. + fn record_sets( + &self, + name: &str, + ) -> impl Future, ZoneError>> + Send; + + /// Applies `changes` as one atomic batch. + /// + /// # Errors + /// + /// Returns [`ZoneError`] when the provider rejects or fails the batch. + fn apply( + &self, + changes: Vec, + ) -> impl Future> + Send; + + /// Waits until the batch is served by every authoritative nameserver. + /// + /// # Errors + /// + /// Returns [`ZoneError`] when the provider call fails or the wait times out. + fn wait_in_sync(&self, change: &ChangeId) + -> impl Future> + Send; +} diff --git a/tests/api.rs b/tests/api.rs new file mode 100644 index 0000000..c9b741a --- /dev/null +++ b/tests/api.rs @@ -0,0 +1,770 @@ +//! HTTP API tests against a fake Route 53, a fake ACME CA and a throwaway Postgres container. + +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; + +use axum::Router; +use axum::body::Body; +use axum::extract::connect_info::MockConnectInfo; +use axum::http::{Method, Request, StatusCode}; +use hosted_dns::{ + AppState, Ca, CaError, Change, ChangeId, Config, MIGRATOR, RecordSet, RecordType, Zone, + ZoneError, reap, router, +}; +use http_body_util::BodyExt; +use serde_json::{Value, json}; +use sqlx::PgPool; +use testcontainers_modules::postgres::Postgres; +use testcontainers_modules::testcontainers::ContainerAsync; +use testcontainers_modules::testcontainers::runners::AsyncRunner; +use tower::ServiceExt; + +const APEX: &str = "ployz.test"; +const MINT_KEY: &str = "test-mint-key"; + +type Sets = HashMap<(String, RecordType), RecordSet>; + +/// Route 53 semantics in memory: batches are atomic, a delete must match +/// exactly, and changes reach the public view (what resolvers see) only once +/// someone waits for them to sync. +#[derive(Clone, Default)] +struct FakeZone { + sets: Arc>, + public: Arc>, + fail_next_apply: Arc>, +} + +impl FakeZone { + fn values(&self, name: &str, kind: RecordType) -> Option> { + let sets = self.sets.lock().unwrap(); + sets.get(&(name.to_owned(), kind)) + .map(|set| set.values.clone()) + } + + fn resolve(&self, name: &str, kind: RecordType) -> Vec { + let public = self.public.lock().unwrap(); + public + .get(&(name.to_owned(), kind)) + .map(|set| set.values.clone()) + .unwrap_or_default() + } + + fn is_empty(&self) -> bool { + self.sets.lock().unwrap().is_empty() + } +} + +impl Zone for FakeZone { + async fn record_sets(&self, name: &str) -> Result, ZoneError> { + let sets = self.sets.lock().unwrap(); + Ok(sets + .values() + .filter(|set| set.name == name) + .cloned() + .collect()) + } + + async fn apply(&self, changes: Vec) -> Result { + if std::mem::take(&mut *self.fail_next_apply.lock().unwrap()) { + return Err(ZoneError("Route 53 is down".into())); + } + let mut sets = self.sets.lock().unwrap(); + let mut next = sets.clone(); + for change in changes { + match change { + Change::Upsert(set) => { + next.insert((set.name.clone(), set.kind), set); + } + Change::Delete(set) => { + if next.remove(&(set.name.clone(), set.kind)).as_ref() != Some(&set) { + return Err(ZoneError(format!("delete of {} does not match", set.name))); + } + } + } + } + *sets = next; + Ok(ChangeId("change".into())) + } + + async fn wait_in_sync(&self, _change: &ChangeId) -> Result<(), ZoneError> { + *self.public.lock().unwrap() = self.sets.lock().unwrap().clone(); + Ok(()) + } +} + +/// An ACME CA that validates DNS-01 by resolving through the fake zone. +#[derive(Clone)] +struct FakeCa { + zone: FakeZone, + rate_limit_next_order: Arc>>, + refuse_next_finalize: Arc>, +} + +struct FakeOrder { + names: Vec, + values: Vec, +} + +impl Ca for FakeCa { + type Order = FakeOrder; + + async fn new_order(&self, names: &[String]) -> Result<(FakeOrder, Vec), CaError> { + if let Some(delay) = self.rate_limit_next_order.lock().unwrap().take() { + return Err(CaError::RateLimited { + until: SystemTime::now() + delay, + }); + } + let values: Vec = names + .iter() + .enumerate() + .map(|(i, name)| format!("dns01-{i}-{name}")) + .collect(); + let order = FakeOrder { + names: names.to_vec(), + values: values.clone(), + }; + Ok((order, values)) + } + + async fn finalize(&self, order: FakeOrder, csr_der: &[u8]) -> Result { + if std::mem::take(&mut *self.refuse_next_finalize.lock().unwrap()) { + return Err(CaError::Failed( + "urn:ietf:params:acme:error:serverInternal".into(), + )); + } + for name in &order.names { + let base = name.trim_start_matches("*."); + let published = self + .zone + .resolve(&format!("_acme-challenge.{base}"), RecordType::Txt); + if !order.values.iter().all(|value| published.contains(value)) { + return Err(CaError::Failed(format!("dns-01 failed for {name}"))); + } + } + let csr = String::from_utf8_lossy(csr_der); + assert!(order.names.iter().all(|name| csr.contains(name.as_str()))); + Ok(format!( + "-----BEGIN CERTIFICATE-----\nleaf for {}\n-----END CERTIFICATE-----\n\ + -----BEGIN CERTIFICATE-----\nintermediate\n-----END CERTIFICATE-----\n", + order.names.join(",") + )) + } +} + +struct Harness { + app: Router, + state: Arc>, + zone: FakeZone, + ca: FakeCa, + db: PgPool, + _postgres: ContainerAsync, +} + +async fn harness(mints_per_hour: u32) -> Harness { + let postgres = Postgres::default().start().await.unwrap(); + let url = format!( + "postgres://postgres:postgres@{}:{}/postgres", + postgres.get_host().await.unwrap(), + postgres.get_host_port_ipv4(5432).await.unwrap() + ); + let db = PgPool::connect(&url).await.unwrap(); + MIGRATOR.run(&db).await.unwrap(); + let zone = FakeZone::default(); + let ca = FakeCa { + zone: zone.clone(), + rate_limit_next_order: Arc::default(), + refuse_next_finalize: Arc::default(), + }; + let config = Config { + apex: APEX.into(), + mints_per_hour, + client_ip_header: Some("x-real-ip".parse().unwrap()), + mint_keys: vec!["other-key".into(), MINT_KEY.into()], + }; + let state = Arc::new(AppState::new(db.clone(), zone.clone(), ca.clone(), config)); + let app = router(Arc::clone(&state)) + .layer(MockConnectInfo(SocketAddr::from(([198, 51, 100, 1], 4000)))); + Harness { + app, + state, + zone, + ca, + db, + _postgres: postgres, + } +} + +impl Harness { + async fn call( + &self, + method: Method, + uri: &str, + headers: &[(&str, &str)], + body: Option, + ) -> (StatusCode, Value) { + let mut request = Request::builder().method(method).uri(uri); + for (key, value) in headers { + request = request.header(*key, *value); + } + let body = match body { + Some(body) => { + request = request.header("content-type", "application/json"); + Body::from(body.to_string()) + } + None => Body::empty(), + }; + let response = self + .app + .clone() + .oneshot(request.body(body).unwrap()) + .await + .unwrap(); + let status = response.status(); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + let body = serde_json::from_slice(&bytes).unwrap_or(Value::Null); + (status, body) + } + + async fn mint(&self, preferred: Option<&str>) -> (String, String) { + let (status, body) = self + .call( + Method::POST, + "/domains", + &[], + Some(json!({ "preferred": preferred })), + ) + .await; + assert_eq!(status, StatusCode::CREATED, "{body}"); + ( + body["name"].as_str().unwrap().to_owned(), + body["token"].as_str().unwrap().to_owned(), + ) + } + + async fn authed( + &self, + method: Method, + name: &str, + path: &str, + token: &str, + body: Option, + ) -> (StatusCode, Value) { + let bearer = format!("Bearer {token}"); + let uri = format!("/domains/{name}{path}"); + self.call(method, &uri, &[("authorization", &bearer)], body) + .await + } + + async fn put_records(&self, name: &str, token: &str, records: Value) -> StatusCode { + self.authed(Method::PUT, name, "/records", token, Some(records)) + .await + .0 + } +} + +fn label_of(name: &str) -> &str { + name.strip_suffix(&format!(".{APEX}")).unwrap() +} + +#[tokio::test] +async fn mint_grants_preferred_suffixed_or_random_labels() { + let h = harness(100).await; + + let (name, token) = h.mint(Some("acme")).await; + assert_eq!(name, format!("acme.{APEX}")); + assert!( + token + .bytes() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()), + "{token}" + ); + + // Taken and reserved labels get a 4-character suffix. + let (taken, other_token) = h.mint(Some("acme")).await; + assert_ne!(token, other_token); + let suffix = label_of(&taken).strip_prefix("acme-").unwrap(); + assert_eq!(suffix.len(), 4); + let (reserved, _) = h.mint(Some("www")).await; + assert!(label_of(&reserved).starts_with("www-"), "{reserved}"); + + // Invalid, edge-hyphen and missing labels get a fully random label. + for preferred in [Some("ab"), Some("-acme"), Some("Acme"), None] { + let (random, _) = h.mint(preferred).await; + let label = label_of(&random); + assert_eq!(label.len(), 10, "{preferred:?} -> {random}"); + assert!(!label.contains('-'), "{random}"); + } + + // No body at all is a random label too. + let (status, body) = h.call(Method::POST, "/domains", &[], None).await; + assert_eq!(status, StatusCode::CREATED); + assert_eq!(label_of(body["name"].as_str().unwrap()).len(), 10); +} + +#[tokio::test] +async fn mint_is_rate_limited_per_source_ip() { + let h = harness(2).await; + for (ip, expected) in [ + ("203.0.113.7", StatusCode::CREATED), + ("203.0.113.7", StatusCode::CREATED), + ("203.0.113.7", StatusCode::TOO_MANY_REQUESTS), + ("203.0.113.8", StatusCode::CREATED), + ] { + let (status, body) = h + .call( + Method::POST, + "/domains", + &[("x-real-ip", ip)], + Some(json!({})), + ) + .await; + assert_eq!(status, expected, "{ip}: {body}"); + } +} + +#[tokio::test] +async fn mint_key_skips_the_limit_and_a_wrong_key_is_refused() { + let h = &harness(1).await; + let mint_with = |authorization: Option<&'static str>| { + let headers: Vec<(&str, &str)> = authorization + .map(|value| ("authorization", value)) + .into_iter() + .collect(); + async move { + h.call(Method::POST, "/domains", &headers, Some(json!({}))) + .await + } + }; + let key = "Bearer test-mint-key"; + + // No header: anonymous and limited. + assert_eq!(mint_with(None).await.0, StatusCode::CREATED); + assert_eq!(mint_with(None).await.0, StatusCode::TOO_MANY_REQUESTS); + // A known key bypasses the limit from the same address. + assert_eq!(mint_with(Some(key)).await.0, StatusCode::CREATED); + assert_eq!(mint_with(Some(key)).await.0, StatusCode::CREATED); + // A wrong or malformed key is refused, not treated as anonymous. + for wrong in ["Bearer test-mint-kez", "Bearer ", "test-mint-key"] { + let (status, body) = mint_with(Some(wrong)).await; + assert_eq!(status, StatusCode::UNAUTHORIZED, "{wrong}"); + assert_eq!(body["error"], "unauthorized"); + } +} + +#[tokio::test] +async fn rotate_kills_the_old_token_at_once() { + let h = harness(100).await; + let (name, old) = h.mint(Some("rotating")).await; + + let (status, body) = h.authed(Method::POST, &name, "/rotate", &old, None).await; + assert_eq!(status, StatusCode::OK); + let new = body["token"].as_str().unwrap().to_owned(); + assert_ne!(new, old); + + let (status, body) = h.authed(Method::POST, &name, "/lease", &old, None).await; + assert_eq!(status, StatusCode::UNAUTHORIZED); + assert_eq!(body["error"], "unauthorized"); + let (status, _) = h.authed(Method::POST, &name, "/lease", &new, None).await; + assert_eq!(status, StatusCode::OK); +} + +#[tokio::test] +async fn authentication_failures_are_distinguished() { + let h = harness(100).await; + let (name, _) = h.mint(Some("guarded")).await; + + let (status, _) = h + .call(Method::POST, &format!("/domains/{name}/lease"), &[], None) + .await; + assert_eq!(status, StatusCode::UNAUTHORIZED); + let (status, _) = h.authed(Method::POST, &name, "/lease", "wrong", None).await; + assert_eq!(status, StatusCode::UNAUTHORIZED); + // Unknown names and names that could never be granted look the same. + for unknown in [ + "nope.ployz.test", + "evil.example.com", + "a.b.ployz.test", + "-x.ployz.test", + "ployz.test", + ] { + let (status, body) = h + .authed(Method::POST, unknown, "/lease", "wrong", None) + .await; + assert_eq!(status, StatusCode::NOT_FOUND, "{unknown}"); + assert_eq!(body["error"], "not_found"); + } +} + +#[tokio::test] +async fn records_put_replaces_the_apex_set_and_writes_the_wildcard() { + let h = harness(100).await; + let (name, token) = h.mint(Some("records")).await; + let wildcard = format!("*.{name}"); + + let both = + json!({ "a": ["203.0.113.2", "203.0.113.1", "203.0.113.2"], "aaaa": ["2001:db8::1"] }); + assert_eq!( + h.put_records(&name, &token, both.clone()).await, + StatusCode::NO_CONTENT + ); + assert_eq!( + h.zone.values(&name, RecordType::A).unwrap(), + ["203.0.113.1", "203.0.113.2"] + ); + assert_eq!( + h.zone.values(&name, RecordType::Aaaa).unwrap(), + ["2001:db8::1"] + ); + assert_eq!( + h.zone.values(&wildcard, RecordType::Cname).unwrap(), + [name.as_str()] + ); + + // Idempotent. + assert_eq!( + h.put_records(&name, &token, both).await, + StatusCode::NO_CONTENT + ); + + // Full set: an omitted family is removed. + let v4_only = json!({ "a": ["198.51.100.9"] }); + assert_eq!( + h.put_records(&name, &token, v4_only).await, + StatusCode::NO_CONTENT + ); + assert_eq!( + h.zone.values(&name, RecordType::A).unwrap(), + ["198.51.100.9"] + ); + assert_eq!(h.zone.values(&name, RecordType::Aaaa), None); + assert_eq!( + h.zone.values(&wildcard, RecordType::Cname).unwrap(), + [name.as_str()] + ); +} + +#[tokio::test] +async fn records_put_refuses_empty_private_and_malformed_sets() { + let h = harness(100).await; + let (name, token) = h.mint(Some("strict")).await; + + for records in [ + json!({}), + json!({ "a": [] }), + json!({ "a": ["10.0.0.1"] }), + json!({ "a": ["127.0.0.1"] }), + json!({ "aaaa": ["fd00::1"] }), + ] { + let (status, body) = h + .authed( + Method::PUT, + &name, + "/records", + &token, + Some(records.clone()), + ) + .await; + assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "{records}"); + assert_eq!(body["error"], "invalid_records"); + } + let (status, body) = h + .authed( + Method::PUT, + &name, + "/records", + &token, + Some(json!({ "a": ["nope"] })), + ) + .await; + assert_eq!(status, StatusCode::BAD_REQUEST); + assert_eq!(body["error"], "invalid_request"); + assert!(h.zone.is_empty()); +} + +#[tokio::test] +async fn authenticated_calls_renew_the_seven_day_lease() { + let h = harness(100).await; + let (name, token) = h.mint(Some("leased")).await; + sqlx::query("UPDATE domains SET lease_expires_at = now() + interval '1 hour'") + .execute(&h.db) + .await + .unwrap(); + + let records = json!({ "a": ["203.0.113.1"] }); + assert_eq!( + h.put_records(&name, &token, records).await, + StatusCode::NO_CONTENT + ); + let renewed: bool = sqlx::query_scalar( + "SELECT lease_expires_at > now() + interval '6 days 23 hours' FROM domains", + ) + .fetch_one(&h.db) + .await + .unwrap(); + assert!(renewed); +} + +#[tokio::test] +async fn release_retires_the_name_forever() { + let h = harness(100).await; + let (name, token) = h.mint(Some("goner")).await; + let records = json!({ "a": ["203.0.113.1"], "aaaa": ["2001:db8::1"] }); + assert_eq!( + h.put_records(&name, &token, records).await, + StatusCode::NO_CONTENT + ); + + let (status, _) = h.authed(Method::DELETE, &name, "", &token, None).await; + assert_eq!(status, StatusCode::NO_CONTENT); + assert!(h.zone.is_empty()); + + let (status, body) = h.authed(Method::POST, &name, "/lease", &token, None).await; + assert_eq!(status, StatusCode::GONE); + assert_eq!(body["error"], "retired"); + let (reissued, _) = h.mint(Some("goner")).await; + assert_ne!(reissued, name); +} + +#[tokio::test] +async fn reaper_frees_unused_reservations_and_retires_expired_leases() { + let h = harness(100).await; + let (unused, unused_token) = h.mint(Some("unused")).await; + let (fresh, fresh_token) = h.mint(Some("fresh")).await; + let (expired, expired_token) = h.mint(Some("expired")).await; + let (live, live_token) = h.mint(Some("live")).await; + let records = json!({ "a": ["203.0.113.1"] }); + assert_eq!( + h.put_records(&expired, &expired_token, records.clone()) + .await, + StatusCode::NO_CONTENT + ); + assert_eq!( + h.put_records(&live, &live_token, records).await, + StatusCode::NO_CONTENT + ); + + sqlx::query("UPDATE domains SET reserved_at = now() - interval '25 hours' WHERE name <> $1") + .bind(&fresh) + .execute(&h.db) + .await + .unwrap(); + sqlx::query( + "UPDATE domains SET lease_expires_at = now() - interval '1 minute' WHERE name = $1", + ) + .bind(&expired) + .execute(&h.db) + .await + .unwrap(); + reap(&h.state).await.unwrap(); + + // Unused for 24 hours: gone, and the label is free again. + let (status, _) = h + .authed(Method::POST, &unused, "/lease", &unused_token, None) + .await; + assert_eq!(status, StatusCode::NOT_FOUND); + assert_eq!(h.mint(Some("unused")).await.0, unused); + // Expired lease: records removed, name retired. + let (status, _) = h + .authed(Method::POST, &expired, "/lease", &expired_token, None) + .await; + assert_eq!(status, StatusCode::GONE); + assert_eq!(h.zone.values(&expired, RecordType::A), None); + assert_eq!( + h.zone.values(&format!("*.{expired}"), RecordType::Cname), + None + ); + // Young reservations and live leases stay. + let (status, _) = h + .authed(Method::POST, &fresh, "/lease", &fresh_token, None) + .await; + assert_eq!(status, StatusCode::OK); + let (status, _) = h + .authed(Method::POST, &live, "/lease", &live_token, None) + .await; + assert_eq!(status, StatusCode::OK); + assert!(h.zone.values(&live, RecordType::A).is_some()); +} + +fn csr(names: &[&str]) -> String { + let key = rcgen::KeyPair::generate().unwrap(); + let names = names + .iter() + .map(|name| (*name).to_owned()) + .collect::>(); + let mut params = rcgen::CertificateParams::new(names).unwrap(); + params.distinguished_name = rcgen::DistinguishedName::new(); + params.serialize_request(&key).unwrap().pem().unwrap() +} + +#[tokio::test] +async fn certificate_is_issued_through_dns01_and_the_challenge_removed() { + let h = harness(100).await; + let (name, token) = h.mint(Some("secure")).await; + let wildcard = format!("*.{name}"); + + let body = json!({ "csr": csr(&[&name, &wildcard]) }); + let (status, body) = h + .authed(Method::POST, &name, "/certificate", &token, Some(body)) + .await; + assert_eq!(status, StatusCode::OK, "{body}"); + let chain = body["certificate_chain_pem"].as_str().unwrap(); + assert!(chain.contains(&format!("leaf for {name},{wildcard}"))); + assert_eq!(chain.matches("BEGIN CERTIFICATE").count(), 2); + + let challenge = format!("_acme-challenge.{name}"); + assert_eq!(h.zone.values(&challenge, RecordType::Txt), None); +} + +#[tokio::test] +async fn certificate_refuses_csrs_outside_the_token_holders_name() { + let h = harness(100).await; + let (name, token) = h.mint(Some("scoped")).await; + let (other, _) = h.mint(Some("victim")).await; + let wildcard = format!("*.{name}"); + let other_wildcard = format!("*.{other}"); + + let with_cn = { + let key = rcgen::KeyPair::generate().unwrap(); + let params = rcgen::CertificateParams::new(vec![name.clone(), wildcard.clone()]).unwrap(); + // rcgen's default subject CN is not one of the names. + params.serialize_request(&key).unwrap().pem().unwrap() + }; + for pem in [ + csr(&[&name]), + csr(&[&wildcard]), + csr(&[&name, &wildcard, &other]), + csr(&[&other, &other_wildcard]), + csr(&[&name, "*.*.scoped.ployz.test"]), + with_cn, + "not a csr".to_owned(), + ] { + let (status, body) = h + .authed( + Method::POST, + &name, + "/certificate", + &token, + Some(json!({ "csr": pem })), + ) + .await; + assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "{body}"); + assert_eq!(body["error"], "invalid_csr"); + } + + // Only the token holder of the name may ask. + let (status, _) = h + .authed( + Method::POST, + &other, + "/certificate", + &token, + Some(json!({ "csr": csr(&[&other, &other_wildcard]) })), + ) + .await; + assert_eq!(status, StatusCode::UNAUTHORIZED); + assert!(h.zone.is_empty()); +} + +#[tokio::test] +async fn certificate_honours_the_cas_retry_after() { + let h = harness(100).await; + let (name, token) = h.mint(Some("patient")).await; + let body = json!({ "csr": csr(&[&name, &format!("*.{name}")]) }); + *h.ca.rate_limit_next_order.lock().unwrap() = Some(Duration::from_secs(120)); + + let request = Request::builder() + .method(Method::POST) + .uri(format!("/domains/{name}/certificate")) + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from(body.to_string())) + .unwrap(); + let response = h.app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); + let retry_after: u64 = response.headers()["retry-after"] + .to_str() + .unwrap() + .parse() + .unwrap(); + assert!((100..=120).contains(&retry_after), "{retry_after}"); + + // The CA would accept now, but the service waits out Retry-After for every name. + let (status, body) = h + .authed(Method::POST, &name, "/certificate", &token, Some(body)) + .await; + assert_eq!(status, StatusCode::TOO_MANY_REQUESTS); + assert_eq!(body["error"], "ca_rate_limited"); +} + +#[tokio::test] +async fn failed_issuance_still_removes_the_challenge() { + let h = harness(100).await; + let (name, token) = h.mint(Some("unlucky")).await; + *h.ca.refuse_next_finalize.lock().unwrap() = true; + + let body = json!({ "csr": csr(&[&name, &format!("*.{name}")]) }); + let (status, body) = h + .authed(Method::POST, &name, "/certificate", &token, Some(body)) + .await; + assert_eq!(status, StatusCode::BAD_GATEWAY); + assert_eq!(body["error"], "ca_error"); + assert_eq!( + h.zone + .values(&format!("_acme-challenge.{name}"), RecordType::Txt), + None + ); +} + +#[tokio::test] +async fn reaper_never_frees_a_name_that_asked_for_a_certificate() { + let h = harness(100).await; + let (name, token) = h.mint(Some("certified")).await; + let body = json!({ "csr": csr(&[&name, &format!("*.{name}")]) }); + let (status, _) = h + .authed(Method::POST, &name, "/certificate", &token, Some(body)) + .await; + assert_eq!(status, StatusCode::OK); + + // No records were ever published, and the reservation is over 24 hours old. + sqlx::query("UPDATE domains SET reserved_at = now() - interval '25 hours'") + .execute(&h.db) + .await + .unwrap(); + reap(&h.state).await.unwrap(); + + let (status, _) = h.authed(Method::POST, &name, "/lease", &token, None).await; + assert_eq!(status, StatusCode::OK); + let (reissued, _) = h.mint(Some("certified")).await; + assert_ne!(reissued, name); +} + +#[tokio::test] +async fn reaper_keeps_an_expired_name_when_route53_fails_and_retries() { + let h = harness(100).await; + let (name, token) = h.mint(Some("stubborn")).await; + let records = json!({ "a": ["203.0.113.1"] }); + assert_eq!( + h.put_records(&name, &token, records).await, + StatusCode::NO_CONTENT + ); + sqlx::query("UPDATE domains SET lease_expires_at = now() - interval '1 minute'") + .execute(&h.db) + .await + .unwrap(); + + *h.zone.fail_next_apply.lock().unwrap() = true; + assert!(reap(&h.state).await.is_err()); + let retired: bool = sqlx::query_scalar("SELECT retired_at IS NOT NULL FROM domains") + .fetch_one(&h.db) + .await + .unwrap(); + assert!(!retired); + assert!(h.zone.values(&name, RecordType::A).is_some()); + + reap(&h.state).await.unwrap(); + let (status, _) = h.authed(Method::POST, &name, "/lease", &token, None).await; + assert_eq!(status, StatusCode::GONE); + assert_eq!(h.zone.values(&name, RecordType::A), None); +}