diff --git a/CHANGELOG.md b/CHANGELOG.md index 90eaad5..cf748e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The minimum supported Rust version (MSRV) is now 1.64. - Manual (and badly designed) threads have been replaced by async. - Randomized early delay, for spacing out renewals when dealing with a lot of certificates. +- Reworked rate-limits, now with scopes for API paths and ACME resources. ## [0.21.0] - 2022-12-19 diff --git a/Cargo.lock b/Cargo.lock index 0c7e16e..2e8674b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,7 +6,6 @@ version = 3 name = "acme_common" version = "0.21.0" dependencies = [ - "attohttpc", "base64", "daemonize", "env_logger", @@ -17,6 +16,7 @@ dependencies = [ "openssl", "openssl-sys", "punycode", + "reqwest", "serde_json", "syslog", "tinytemplate", @@ -30,15 +30,18 @@ dependencies = [ "acme_common", "async-lock", "async-process", - "attohttpc", "bincode", "clap", "futures", "glob", + "governor", + "itertools", "log", "nix", "nom", "rand", + "regex", + "reqwest", "serde", "serde_json", "tinytemplate", @@ -165,22 +168,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" -[[package]] -name = "attohttpc" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" -dependencies = [ - "encoding_rs", - "encoding_rs_io", - "http", - "log", - "native-tls", - "serde", - "serde_json", - "url", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -223,6 +210,12 @@ dependencies = [ "log", ] +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + [[package]] name = "bytes" version = "1.4.0" @@ -327,6 +320,12 @@ dependencies = [ "libc", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -336,15 +335,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - [[package]] name = "env_logger" version = "0.10.0" @@ -504,7 +494,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.13", ] [[package]] @@ -519,6 +509,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -554,6 +550,40 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "governor" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +dependencies = [ + "cfg-if", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -597,12 +627,72 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.3.0" @@ -643,6 +733,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + [[package]] name = "is-terminal" version = "0.4.7" @@ -655,12 +751,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -719,6 +833,12 @@ dependencies = [ "autocfg", ] +[[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" @@ -769,6 +889,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -779,6 +905,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num_cpus" version = "1.15.0" @@ -827,7 +959,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.13", ] [[package]] @@ -1022,6 +1154,43 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "reqwest" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustix" version = "0.37.11" @@ -1097,7 +1266,7 @@ checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.13", ] [[package]] @@ -1120,6 +1289,18 @@ dependencies = [ "serde", ] +[[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 = "signal-hook" version = "0.3.15" @@ -1176,6 +1357,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.13" @@ -1313,7 +1505,31 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.13", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", ] [[package]] @@ -1350,6 +1566,38 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1406,12 +1654,98 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1598,3 +1932,12 @@ checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/acme_common/Cargo.toml b/acme_common/Cargo.toml index 325a57b..2ad86c3 100644 --- a/acme_common/Cargo.toml +++ b/acme_common/Cargo.toml @@ -19,7 +19,6 @@ openssl_dyn = ["crypto_openssl", "openssl", "openssl-sys"] openssl_vendored = ["crypto_openssl", "openssl/vendored", "openssl-sys/vendored"] [dependencies] -attohttpc = { version = "0.24", default-features = false } base64 = "0.21" daemonize = "0.5" env_logger = "0.10" @@ -29,6 +28,7 @@ native-tls = "0.2" openssl = { version = "0.10", optional = true } openssl-sys = { version = "0.9", optional = true } punycode = "0.4" +reqwest = { version = "0.11.16", default-features = false } serde_json = "1.0" syslog = "6.0" tinytemplate = "1.2" diff --git a/acme_common/src/error.rs b/acme_common/src/error.rs index 44cf1c0..18ca131 100644 --- a/acme_common/src/error.rs +++ b/acme_common/src/error.rs @@ -93,8 +93,8 @@ impl From for Error { } } -impl From for Error { - fn from(error: attohttpc::Error) -> Self { +impl From for Error { + fn from(error: reqwest::Error) -> Self { format!("HTTP error: {error}").into() } } diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index a6d097f..f19f5c1 100644 --- a/acmed/Cargo.toml +++ b/acmed/Cargo.toml @@ -16,14 +16,13 @@ publish = false [features] default = ["openssl_dyn"] crypto_openssl = [] -openssl_dyn = ["crypto_openssl", "acme_common/openssl_dyn", "attohttpc/tls"] -openssl_vendored = ["crypto_openssl", "acme_common/openssl_vendored", "attohttpc/tls", "attohttpc/tls-vendored"] +openssl_dyn = ["crypto_openssl", "acme_common/openssl_dyn"] +openssl_vendored = ["crypto_openssl", "acme_common/openssl_vendored"] [dependencies] acme_common = { path = "../acme_common" } async-lock = "2.6" async-process = "1.6" -attohttpc = { version = "0.24", default-features = false, features = ["charsets", "json"] } bincode = "1.3" clap = { version = "4.0", features = ["string"] } futures = "0.3" @@ -36,6 +35,10 @@ tinytemplate = "1.2" toml = "0.7" tokio = { version = "1", features = ["full"] } rand = "0.8.5" +reqwest = "0.11.16" +governor = { version = "0.5.1", default-features = false, features = ["std"] } +regex = "1.7.3" +itertools = "0.10.5" [target.'cfg(unix)'.dependencies] nix = "0.26" diff --git a/acmed/build.rs b/acmed/build.rs index 2e0fed1..631c03d 100644 --- a/acmed/build.rs +++ b/acmed/build.rs @@ -96,7 +96,7 @@ fn set_lock() { } }; for p in lock.package.iter() { - if p.name == "attohttpc" { + if p.name == "reqwest" { let agent = format!("{}/{}", p.name, p.version); set_rustc_env_var!("ACMED_HTTP_LIB_AGENT", agent); set_rustc_env_var!("ACMED_HTTP_LIB_NAME", p.name); diff --git a/acmed/config/letsencrypt.toml b/acmed/config/letsencrypt.toml index 60700f4..659abf3 100644 --- a/acmed/config/letsencrypt.toml +++ b/acmed/config/letsencrypt.toml @@ -1,16 +1,43 @@ [[rate-limit]] -name = "Let's Encrypt rate-limit" +name = "Let's Encrypt newOrder" +acme_resources = ["newOrder"] +number = 300 +period = "3h" + +[[rate-limit]] +name = "Let's Encrypt overall named resources" +acme_resources = ["newNonce", "newAccount", "newOrder", "revokeCert"] number = 20 period = "1s" +[[rate-limit]] +name = "Let's Encrypt overall path prefix" +path = "^https://acmed-v02.api\.letsencrypt\.org/(directory)|(acme/.*)$" +number = 40 +period = "1s" + [[endpoint]] name = "Let's Encrypt v2 production" url = "https://acme-v02.api.letsencrypt.org/directory" -rate_limits = ["Let's Encrypt rate-limit"] +rate_limits = [ + "Let's Encrypt newOrder", + "Let's Encrypt overall named resources", + "Let's Encrypt overall path prefix" +] tos_agreed = false +[[rate-limit]] +name = "Let's Encrypt newOrder staging" +acme_resources = ["newOrder"] +number = 300 +period = "3h" + [[endpoint]] name = "Let's Encrypt v2 staging" url = "https://acme-staging-v02.api.letsencrypt.org/directory" -rate_limits = ["Let's Encrypt rate-limit"] +rate_limits = [ + "Let's Encrypt newOrder staging", + "Let's Encrypt overall named resources", + "Let's Encrypt overall path prefix" +] tos_agreed = false diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index 1ddffd7..b3dd8f3 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -94,7 +94,9 @@ pub async fn request_certificate( let endpoint_name = endpoint_s.read().await.name.clone(); // Refresh the directory - http::refresh_directory(&mut *(endpoint_s.write().await)).map_err(HttpError::in_err)?; + http::refresh_directory(&mut *(endpoint_s.write().await)) + .await + .map_err(HttpError::in_err)?; // Synchronize the account account_s @@ -109,7 +111,7 @@ pub async fn request_certificate( let new_order = NewOrder::new(&cert.identifiers); let new_order = serde_json::to_string(&new_order)?; let data_builder = set_data_builder!(account_s, endpoint_name, new_order.as_bytes()).await; - match http::new_order(&mut *(endpoint_s.write().await), &data_builder) { + match http::new_order(&mut *(endpoint_s.write().await), &data_builder).await { Ok((order, order_url)) => { if let Some(e) = order.get_error() { cert.warn(&e.prefix("Error").message); @@ -138,6 +140,7 @@ pub async fn request_certificate( let data_builder = set_data_builder!(account_s, endpoint_name, b"").await; let auth = http::get_authorization(&mut *(endpoint_s.write().await), &data_builder, auth_url) + .await .map_err(HttpError::in_err)?; drop(data_builder); if let Some(e) = auth.get_error() { @@ -177,7 +180,9 @@ pub async fn request_certificate( &mut *(endpoint_s.write().await), &data_builder, &chall_url, + None, ) + .await .map_err(HttpError::in_err)?; drop(data_builder); } @@ -192,6 +197,7 @@ pub async fn request_certificate( &break_fn, auth_url, ) + .await .map_err(HttpError::in_err)?; drop(data_builder); for (data, hook_type) in hook_datas.iter() { @@ -211,6 +217,7 @@ pub async fn request_certificate( &break_fn, &order_url, ) + .await .map_err(HttpError::in_err)?; drop(data_builder); @@ -246,6 +253,7 @@ pub async fn request_certificate( &data_builder, &order.finalize, ) + .await .map_err(HttpError::in_err)?; drop(data_builder); if let Some(e) = order.get_error() { @@ -261,6 +269,7 @@ pub async fn request_certificate( &break_fn, &order_url, ) + .await .map_err(HttpError::in_err)?; drop(data_builder); @@ -270,6 +279,7 @@ pub async fn request_certificate( .ok_or_else(|| Error::from("no certificate available for download"))?; let data_builder = set_data_builder!(account_s, endpoint_name, b"").await; let crt = http::get_certificate(&mut *(endpoint_s.write().await), &data_builder, &crt_url) + .await .map_err(HttpError::in_err)?; drop(data_builder); storage::write_certificate(&cert.file_manager, crt.as_bytes()).await?; diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index aace673..558f479 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -52,8 +52,9 @@ pub async fn register_account( Some(n.to_string()), ) }; - let (acc_rep, account_url) = - http::new_account(endpoint, &data_builder).map_err(HttpError::in_err)?; + let (acc_rep, account_url) = http::new_account(endpoint, &data_builder) + .await + .map_err(HttpError::in_err)?; account.set_account_url(&endpoint.name, &account_url)?; let orders_url = match acc_rep.orders { Some(url) => url, @@ -95,7 +96,7 @@ pub async fn update_account_contacts( set_data_builder_sync!(account_owned, endpoint_name, acc_up_struct.as_bytes()); let url = account.get_endpoint(&endpoint_name)?.account_url.clone(); create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, &data_builder, &url), + http::post_jose_no_response(endpoint, &data_builder, &url, None).await, endpoint, account )?; @@ -140,7 +141,7 @@ pub async fn update_account_key( ) }; create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, &data_builder, &url), + http::post_jose_no_response(endpoint, &data_builder, &url, None).await, endpoint, account )?; diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index 022ec42..1d31ecd 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -1,14 +1,15 @@ use crate::acme_proto::structs::{AccountResponse, Authorization, Directory, Order}; +use crate::config::NamedAcmeResource; use crate::endpoint::Endpoint; use crate::http; use acme_common::error::Error; use std::{thread, time}; macro_rules! pool_object { - ($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $data_builder: expr, $break: expr) => {{ + ($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $resource: expr, $data_builder: expr, $break: expr) => {{ for _ in 0..crate::DEFAULT_POOL_NB_TRIES { thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); - let response = http::post_jose($endpoint, $url, $data_builder)?; + let response = http::post_jose($endpoint, $url, $resource, $data_builder).await?; let obj = response.json::<$obj_type>()?; if $break(&obj) { return Ok(obj); @@ -19,26 +20,27 @@ macro_rules! pool_object { }}; } -pub fn refresh_directory(endpoint: &mut Endpoint) -> Result<(), http::HttpError> { +pub async fn refresh_directory(endpoint: &mut Endpoint) -> Result<(), http::HttpError> { let url = endpoint.url.clone(); - let response = http::get(endpoint, &url)?; + let response = http::get(endpoint, &url, Some(NamedAcmeResource::Directory)).await?; endpoint.dir = response.json::()?; Ok(()) } -pub fn post_jose_no_response( +pub async fn post_jose_no_response( endpoint: &mut Endpoint, data_builder: &F, url: &str, + resource: Option, ) -> Result<(), http::HttpError> where F: Fn(&str, &str) -> Result, { - let _ = http::post_jose(endpoint, url, data_builder)?; + let _ = http::post_jose(endpoint, url, resource, data_builder).await?; Ok(()) } -pub fn new_account( +pub async fn new_account( endpoint: &mut Endpoint, data_builder: &F, ) -> Result<(AccountResponse, String), http::HttpError> @@ -46,7 +48,13 @@ where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_account.clone(); - let response = http::post_jose(endpoint, &url, data_builder)?; + let response = http::post_jose( + endpoint, + &url, + Some(NamedAcmeResource::NewAccount), + data_builder, + ) + .await?; let acc_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -54,7 +62,7 @@ where Ok((acc_resp, acc_uri)) } -pub fn new_order( +pub async fn new_order( endpoint: &mut Endpoint, data_builder: &F, ) -> Result<(Order, String), http::HttpError> @@ -62,7 +70,13 @@ where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_order.clone(); - let response = http::post_jose(endpoint, &url, data_builder)?; + let response = http::post_jose( + endpoint, + &url, + Some(NamedAcmeResource::NewOrder), + data_builder, + ) + .await?; let order_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -70,7 +84,7 @@ where Ok((order_resp, order_uri)) } -pub fn get_authorization( +pub async fn get_authorization( endpoint: &mut Endpoint, data_builder: &F, url: &str, @@ -78,12 +92,12 @@ pub fn get_authorization( where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, url, data_builder)?; + let response = http::post_jose(endpoint, url, None, data_builder).await?; let auth = response.json::()?; Ok(auth) } -pub fn pool_authorization( +pub async fn pool_authorization( endpoint: &mut Endpoint, data_builder: &F, break_fn: &S, @@ -98,12 +112,13 @@ where "authorization", endpoint, url, + None, data_builder, break_fn ) } -pub fn pool_order( +pub async fn pool_order( endpoint: &mut Endpoint, data_builder: &F, break_fn: &S, @@ -113,10 +128,10 @@ where F: Fn(&str, &str) -> Result, S: Fn(&Order) -> bool, { - pool_object!(Order, "order", endpoint, url, data_builder, break_fn) + pool_object!(Order, "order", endpoint, url, None, data_builder, break_fn) } -pub fn finalize_order( +pub async fn finalize_order( endpoint: &mut Endpoint, data_builder: &F, url: &str, @@ -124,12 +139,12 @@ pub fn finalize_order( where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, url, data_builder)?; + let response = http::post_jose(endpoint, url, None, data_builder).await?; let order = response.json::()?; Ok(order) } -pub fn get_certificate( +pub async fn get_certificate( endpoint: &mut Endpoint, data_builder: &F, url: &str, @@ -140,9 +155,11 @@ where let response = http::post( endpoint, url, + None, data_builder, http::CONTENT_TYPE_JOSE, http::CONTENT_TYPE_PEM, - )?; + ) + .await?; Ok(response.body) } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 032654d..59f812b 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -12,6 +12,7 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt; use std::fs::{self, File}; use std::io::prelude::*; +use std::num::NonZeroU32; use std::path::{Path, PathBuf}; use std::result::Result; use std::time::Duration; @@ -72,10 +73,10 @@ pub struct Config { } impl Config { - fn get_rate_limit(&self, name: &str) -> Result<(usize, String), Error> { + fn get_rate_limit(&self, name: &str) -> Result { for rl in self.rate_limit.iter() { if rl.name == name { - return Ok((rl.number, rl.period.to_owned())); + return Ok(rl.clone()); } } Err(format!("{name}: rate limit not found").into()) @@ -266,8 +267,7 @@ impl Endpoint { ) -> Result { let mut limits = vec![]; for rl_name in self.rate_limits.iter() { - let (nb, timeframe) = cnf.get_rate_limit(rl_name)?; - limits.push((nb, timeframe)); + limits.push(cnf.get_rate_limit(rl_name)?); } let mut root_lst: Vec = vec![]; root_lst.extend(root_certs.iter().map(|v| v.to_string())); @@ -293,8 +293,23 @@ impl Endpoint { #[serde(deny_unknown_fields)] pub struct RateLimit { pub name: String, - pub number: usize, + pub number: NonZeroU32, pub period: String, + #[serde(default)] + pub acme_resources: Vec, + pub path: Option, +} + +#[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub enum NamedAcmeResource { + Directory, + NewNonce, + NewAccount, + NewOrder, + NewAuthz, + RevokeCert, + KeyChange, } #[derive(Deserialize)] diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index a4016c1..8e7fa7f 100644 --- a/acmed/src/endpoint.rs +++ b/acmed/src/endpoint.rs @@ -1,17 +1,24 @@ -use crate::acme_proto::structs::Directory; +use crate::config::NamedAcmeResource; use crate::duration::parse_duration; +use crate::{acme_proto::structs::Directory, config}; use acme_common::error::Error; -use std::cmp; -use std::thread; -use std::time::{Duration, Instant}; +use governor::{ + clock::DefaultClock, + state::{direct::NotKeyed, InMemoryState}, + Quota, RateLimiter, +}; +use itertools::Itertools; +use regex::Regex; +use std::convert::{TryFrom, TryInto}; +use std::time::Duration; -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Endpoint { pub name: String, pub url: String, pub tos_agreed: bool, pub nonce: Option, - pub rl: RateLimit, + pub rl: RateLimits, pub dir: Directory, pub root_certificates: Vec, } @@ -21,7 +28,7 @@ impl Endpoint { name: &str, url: &str, tos_agreed: bool, - limits: &[(usize, String)], + limits: &[config::RateLimit], root_certs: &[String], ) -> Result { Ok(Self { @@ -29,7 +36,7 @@ impl Endpoint { url: url.to_string(), tos_agreed, nonce: None, - rl: RateLimit::new(limits)?, + rl: RateLimits::new(limits)?, dir: Directory { meta: None, new_nonce: String::new(), @@ -44,87 +51,121 @@ impl Endpoint { } } -#[derive(Clone, Debug)] -pub struct RateLimit { - limits: Vec<(usize, Duration)>, - query_log: Vec, +#[derive(Debug)] +pub struct RateLimits { + limits: Vec, } -impl RateLimit { - pub fn new(raw_limits: &[(usize, String)]) -> Result { - let mut limits = vec![]; - for (nb, raw_duration) in raw_limits.iter() { - let parsed_duration = parse_duration(raw_duration)?; - limits.push((*nb, parsed_duration)); - } - limits.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - limits.reverse(); - Ok(Self { - limits, - query_log: vec![], - }) +impl RateLimits { + pub fn new(raw_limits: &[config::RateLimit]) -> Result { + let limits: Result, Error> = raw_limits + .iter() + .sorted_by(rate_limit_cmp) + // We're reverting the comparison here, as we want to get the strongest limits (those + // with the highest waiting period per request) first. + .rev() + .map(|raw| raw.try_into()) + .collect(); + Ok(Self { limits: limits? }) } - pub fn block_until_allowed(&mut self) { - if self.limits.is_empty() { - return; - } - let sleep_duration = self.get_sleep_duration(); - loop { - self.prune_log(); - if self.request_allowed() { - self.query_log.push(Instant::now()); - return; + pub async fn block_until_allowed(&mut self, resource: Option, path: &str) { + for limit in &self.limits { + if limit.matches(resource, path) { + limit.until_ready().await } - // TODO: find a better sleep duration - thread::sleep(sleep_duration); } } +} - fn get_sleep_duration(&self) -> Duration { - let (nb_req, min_duration) = match self.limits.last() { - Some((n, d)) => (*n as u64, *d), - None => { - return Duration::from_millis(0); - } - }; - let nb_mili = match min_duration.as_secs() { - 0 | 1 => crate::MIN_RATE_LIMIT_SLEEP_MILISEC, - n => { - let a = n * 200 / nb_req; - let a = cmp::min(a, crate::MAX_RATE_LIMIT_SLEEP_MILISEC); - cmp::max(a, crate::MIN_RATE_LIMIT_SLEEP_MILISEC) - } +fn rate_limit_cmp(a: &&config::RateLimit, b: &&config::RateLimit) -> std::cmp::Ordering { + let a_dur = parse_duration(&a.period).unwrap_or(Duration::ZERO) / u32::from(a.number); + let b_dur = parse_duration(&b.period).unwrap_or(Duration::ZERO) / u32::from(b.number); + + // A limit is "stronger" if it's period is long. The duration calculated here is the time + // per request. A shorter duration to wait, hence more requests, is *less* of a limit, so + // directly using the result of the comparison of the two calculated durations is correct. + Ord::cmp(&a_dur, &b_dur) +} + +#[derive(Debug)] +pub struct RateLimit { + limiter: RateLimiter, + resources: Vec, + path: Option, +} + +impl RateLimit { + fn matches(&self, resource: Option, path: &str) -> bool { + let resource_matches = resource + .map(|resource| self.resources.contains(&resource)) + .unwrap_or(false); + let path_matches = self + .path + .as_ref() + .map(|matcher| matcher.is_match(path)) + .unwrap_or(false); + resource_matches || path_matches + } + async fn until_ready(&self) { + self.limiter.until_ready().await + } +} + +impl TryFrom<&config::RateLimit> for RateLimit { + type Error = Error; + + fn try_from(value: &config::RateLimit) -> Result { + let period = parse_duration(&value.period)?; + let amount = value.number; + let quota = Quota::with_period(period / u32::from(amount)) + .ok_or("rate-limit period was passed as zero, which is illegal")? + .allow_burst(amount); + let limiter = RateLimiter::direct(quota); + let path = match &value.path { + Some(path) => Some(Regex::new(path).map_err(|e| e.to_string())?), + None => None, }; - Duration::from_millis(nb_mili) + Ok(Self { + limiter, + resources: value.acme_resources.clone(), + path, + }) } +} - fn request_allowed(&self) -> bool { - for (max_allowed, duration) in self.limits.iter() { - match Instant::now().checked_sub(*duration) { - Some(max_date) => { - let nb_req = self - .query_log - .iter() - .filter(move |x| **x > max_date) - .count(); - if nb_req >= *max_allowed { - return false; - } - } - None => { - return false; - } - }; - } - true +#[cfg(test)] +mod tests { + use std::{cmp::Ordering, num::NonZeroU32}; + + use crate::config; + + #[test] + fn check_ratelimit_ordering() { + let sixty_per_hour = cfg_ratelimit_helper(NonZeroU32::new(60).unwrap(), "1h".into()); + let one_per_minute = cfg_ratelimit_helper(NonZeroU32::new(1).unwrap(), "1m".into()); + let one_per_second = cfg_ratelimit_helper(NonZeroU32::new(1).unwrap(), "1s".into()); + assert_eq!( + super::rate_limit_cmp(&&sixty_per_hour, &&one_per_minute), + Ordering::Equal + ); + assert_eq!( + super::rate_limit_cmp(&&one_per_second, &&one_per_minute), + Ordering::Less + ); + assert_eq!( + super::rate_limit_cmp(&&sixty_per_hour, &&one_per_second), + Ordering::Greater + ); } - fn prune_log(&mut self) { - if let Some((_, max_limit)) = self.limits.first() { - if let Some(prune_date) = Instant::now().checked_sub(*max_limit) { - self.query_log.retain(move |&d| d > prune_date); - } + fn cfg_ratelimit_helper(number: NonZeroU32, period: String) -> config::RateLimit { + config::RateLimit { + name: String::new(), + number, + period, + acme_resources: vec![], + path: None, } } } diff --git a/acmed/src/http.rs b/acmed/src/http.rs index 7b0c048..b6cf8d6 100644 --- a/acmed/src/http.rs +++ b/acmed/src/http.rs @@ -1,9 +1,10 @@ use crate::acme_proto::structs::{AcmeError, HttpApiError}; +use crate::config::NamedAcmeResource; use crate::endpoint::Endpoint; #[cfg(feature = "crypto_openssl")] -use acme_common::crypto::X509Certificate; use acme_common::error::Error; -use attohttpc::{charsets, header, Response, Session}; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::{header, Client, ClientBuilder, Response}; use std::fs::File; #[cfg(feature = "crypto_openssl")] use std::io::prelude::*; @@ -16,7 +17,7 @@ pub const HEADER_NONCE: &str = "Replay-Nonce"; pub const HEADER_LOCATION: &str = "Location"; pub struct ValidHttpResponse { - headers: attohttpc::header::HeaderMap, + headers: HeaderMap, pub body: String, } @@ -38,9 +39,9 @@ impl ValidHttpResponse { serde_json::from_str(&self.body).map_err(Error::from) } - fn from_response(response: Response) -> Result { - let (_status, headers, body) = response.split(); - let body = body.text()?; + async fn from_response(response: Response) -> Result { + let headers = response.headers().clone(); + let body = response.text().await?; log::trace!("HTTP response headers: {headers:?}"); log::trace!("HTTP response body: {body}"); Ok(ValidHttpResponse { headers, body }) @@ -93,8 +94,8 @@ impl From for HttpError { } } -impl From for HttpError { - fn from(error: attohttpc::Error) -> Self { +impl From for HttpError { + fn from(error: reqwest::Error) -> Self { HttpError::GenericError(error.into()) } } @@ -106,10 +107,9 @@ fn is_nonce(data: &str) -> bool { .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_') } -fn new_nonce(endpoint: &mut Endpoint) -> Result<(), HttpError> { - rate_limit(endpoint); +async fn new_nonce(endpoint: &mut Endpoint) -> Result<(), HttpError> { let url = endpoint.dir.new_nonce.clone(); - let _ = get(endpoint, &url)?; + let _ = get(endpoint, &url, Some(NamedAcmeResource::NewNonce)).await?; Ok(()) } @@ -126,7 +126,7 @@ fn update_nonce(endpoint: &mut Endpoint, response: &Response) -> Result<(), Erro } fn check_status(response: &Response) -> Result<(), Error> { - if !response.is_success() { + if !response.status().is_success() { let status = response.status(); let msg = format!("HTTP error: {}: {}", status.as_u16(), status.as_str()); return Err(msg.into()); @@ -134,18 +134,18 @@ fn check_status(response: &Response) -> Result<(), Error> { Ok(()) } -fn rate_limit(endpoint: &mut Endpoint) { - endpoint.rl.block_until_allowed(); +async fn rate_limit(endpoint: &mut Endpoint, resource: Option, path: &str) { + endpoint.rl.block_until_allowed(resource, path).await; } -fn header_to_string(header_value: &header::HeaderValue) -> Result { +fn header_to_string(header_value: &HeaderValue) -> Result { let s = header_value .to_str() .map_err(|_| Error::from("invalid header format"))?; Ok(s.to_string()) } -fn get_session(root_certs: &[String]) -> Result { +fn get_client(root_certs: &[String]) -> Result { let useragent = format!( "{}/{} ({}) {}", crate::APP_NAME, @@ -154,10 +154,11 @@ fn get_session(root_certs: &[String]) -> Result { env!("ACMED_HTTP_LIB_AGENT") ); // TODO: allow to change the language - let mut session = Session::new(); - session.default_charset(Some(charsets::UTF_8)); - session.try_header(header::ACCEPT_LANGUAGE, "en-US,en;q=0.5")?; - session.try_header(header::USER_AGENT, &useragent)?; + let mut client_builder = ClientBuilder::new(); + let mut default_headers = HeaderMap::new(); + default_headers.append(header::ACCEPT_LANGUAGE, "en-US,en;q=0.5".parse().unwrap()); + default_headers.append(header::USER_AGENT, useragent.parse().unwrap()); + client_builder = client_builder.default_headers(default_headers); for crt_file in root_certs.iter() { #[cfg(feature = "crypto_openssl")] { @@ -165,26 +166,36 @@ fn get_session(root_certs: &[String]) -> Result { File::open(crt_file) .map_err(|e| Error::from(e).prefix(crt_file))? .read_to_end(&mut buff)?; - let crt = X509Certificate::from_pem_native(&buff)?; - session.add_root_certificate(crt); + let crt = reqwest::Certificate::from_pem(&buff)?; + client_builder = client_builder.add_root_certificate(crt); } } - Ok(session) + Ok(client_builder.build()?) } -pub fn get(endpoint: &mut Endpoint, url: &str) -> Result { - let mut session = get_session(&endpoint.root_certificates)?; - session.try_header(header::ACCEPT, CONTENT_TYPE_JSON)?; - rate_limit(endpoint); - let response = session.get(url).send()?; +pub async fn get( + endpoint: &mut Endpoint, + url: &str, + resource: Option, +) -> Result { + let client = get_client(&endpoint.root_certificates)?; + rate_limit(endpoint, resource, url).await; + let response = client + .get(url) + .header(header::ACCEPT, CONTENT_TYPE_JSON) + .send() + .await?; update_nonce(endpoint, &response)?; check_status(&response)?; - ValidHttpResponse::from_response(response).map_err(HttpError::from) + ValidHttpResponse::from_response(response) + .await + .map_err(HttpError::from) } -pub fn post( +pub async fn post( endpoint: &mut Endpoint, url: &str, + resource: Option, data_builder: &F, content_type: &str, accept: &str, @@ -192,25 +203,28 @@ pub fn post( where F: Fn(&str, &str) -> Result, { - let mut session = get_session(&endpoint.root_certificates)?; - session.try_header(header::ACCEPT, accept)?; - session.try_header(header::CONTENT_TYPE, content_type)?; + let client = get_client(&endpoint.root_certificates)?; if endpoint.nonce.is_none() { - let _ = new_nonce(endpoint); + let _ = new_nonce(endpoint).await; } for _ in 0..crate::DEFAULT_HTTP_FAIL_NB_RETRY { + let mut request = client.post(url); + request = request.header(header::ACCEPT, accept); + request = request.header(header::CONTENT_TYPE, content_type); let nonce = &endpoint.nonce.clone().unwrap_or_default(); let body = data_builder(nonce, url)?; - rate_limit(endpoint); + rate_limit(endpoint, resource, url).await; log::trace!("POST request body: {body}"); - let response = session.post(url).text(&body).send()?; + let response = request.body(body).send().await?; update_nonce(endpoint, &response)?; match check_status(&response) { Ok(_) => { - return ValidHttpResponse::from_response(response).map_err(HttpError::from); + return ValidHttpResponse::from_response(response) + .await + .map_err(HttpError::from); } Err(_) => { - let resp = ValidHttpResponse::from_response(response)?; + let resp = ValidHttpResponse::from_response(response).await?; let api_err = resp.json::()?; let acme_err = api_err.get_acme_type(); if !acme_err.is_recoverable() { @@ -223,9 +237,10 @@ where Err("too much errors, will not retry".into()) } -pub fn post_jose( +pub async fn post_jose( endpoint: &mut Endpoint, url: &str, + resource: Option, data_builder: &F, ) -> Result where @@ -234,10 +249,12 @@ where post( endpoint, url, + resource, data_builder, CONTENT_TYPE_JOSE, CONTENT_TYPE_JSON, ) + .await } #[cfg(test)] diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 9d95c69..fa61e33 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -141,12 +141,12 @@ impl MainEventLoop { Ok(MainEventLoop { certificates, accounts: accounts - .iter() - .map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned())))) + .into_iter() + .map(|(k, v)| (k, Arc::new(RwLock::new(v)))) .collect(), endpoints: endpoints - .iter() - .map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned())))) + .into_iter() + .map(|(k, v)| (k, Arc::new(RwLock::new(v)))) .collect(), }) } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 35b6ba6..19adf41 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -309,7 +309,7 @@ Specify the user who will own newly-created private-key files. See for more details. .It Cm random_early_renew Ar string Period of time before the usual certificate renewal, in which the certificate will renew at a random time. This is useful for when -you want to even out your certificate orders when you're dealing with very large numbers of certificates. The format is described in the +you want to even outoyour certificate orders when you're dealing with very large numbers of certificates. The format is described in the .Sx TIME PERIODS section. By default, this is disabled, or rather, the time frame is set to 0. .It Cm renew_delay Ar string @@ -391,17 +391,39 @@ and all three defines the same global option, the final value will be the one de .Pp Unix style globing is supported. .It Ic rate-limit -Array of table where each element defines a HTTPS rate limit. +Array of table where each element defines a HTTPS rate limit. For a rate-limit to apply, the request must match the limit by requesting one of the named ACME resources listed in +.Em acme_resources +or by having a path matching the regular expression in +.Em path . .Bl -tag .It Cm name Ar string The name the rate limit is registered under. Must be unique. .It Cm number Ar integer -Number of requests authorized withing the time period. +Number of requests authorized withing the time period. The amount of requests must be non-zero. .It Cm period Ar string Period of time during which a maximal number of requests is authorized. The format is described in the .Sx TIME PERIODS -section. +section. The period must be non-zero. +.It Cm acme_resources Ar array +Array of strings, containing named ACME resources that this limit applies to. Possible values are: +.Bl -dash -compact +.It +directory +.It +newNonce +.It +newAccount +.It +newOrder +.It +newAuthz +.It +revokeCert +.It +keyChange .El +.It Cm path Ar string +A regular expression matching the paths that this rate-limit should apply to. .El .Sh WRITING A HOOK When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every identifier to be included: it requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: although several default methods can be implemented, sometimes admins wants or are required to do it in a different way.