diff --git a/.rustfmt.toml b/.rustfmt.toml index 750ac8a..d1afa00 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -6,3 +6,6 @@ reorder_modules = true match_block_trailing_comma = true use_field_init_shorthand = true use_small_heuristics = "Off" +# clean up multiple use crate:: that share the same root import +# run with cargo +nightly fmt +imports_granularity = "Crate" diff --git a/Cargo.lock b/Cargo.lock index c773153..5cd9b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -146,7 +146,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", ] @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -203,7 +203,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] @@ -427,6 +426,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -517,6 +533,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -533,6 +559,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -725,6 +760,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -803,6 +847,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[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" @@ -924,8 +977,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -936,10 +991,24 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "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", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + [[package]] name = "getset" version = "0.1.6" @@ -1036,9 +1105,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1123,18 +1192,23 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1161,12 +1235,115 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +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" @@ -1194,7 +1371,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" dependencies = [ "async-trait", - "aws-lc-rs", "base64", "bytes", "http", @@ -1205,14 +1381,21 @@ dependencies = [ "hyper-rustls", "hyper-util", "rcgen", + "ring", "rustls", "rustls-pki-types", "serde", "serde_json", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", ] +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1246,7 +1429,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1337,6 +1520,12 @@ dependencies = [ "libc", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "local-ip-address" version = "0.6.12" @@ -1372,6 +1561,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.8.0" @@ -1387,6 +1582,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" @@ -1869,6 +2070,15 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1940,18 +2150,79 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" name = "proxy" version = "0.2.3" dependencies = [ + "arc-swap", "async-trait", "chrono", "http", "instant-acme", "pingora", "rcgen", + "reqwest", + "rustls", "serde", - "thiserror 2.0.19", + "serde_json", + "thiserror 2.0.20", "tokio", "toml", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1967,6 +2238,12 @@ 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.6" @@ -1988,6 +2265,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2026,13 +2314,27 @@ 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 = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rcgen" version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ - "aws-lc-rs", "pem", "ring", "rustls-pki-types", @@ -2079,6 +2381,46 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2089,7 +2431,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -2154,12 +2496,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2184,6 +2528,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] @@ -2193,7 +2538,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -2223,7 +2568,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -2269,7 +2614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2323,9 +2668,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2427,6 +2772,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -2494,6 +2845,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2505,6 +2865,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2516,11 +2897,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -2536,9 +2917,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -2595,6 +2976,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" @@ -2670,9 +3076,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -2694,9 +3100,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -2707,6 +3113,45 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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" @@ -2783,15 +3228,27 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "untrusted" -version = "0.9.0" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" @@ -2852,6 +3309,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.121" @@ -2884,6 +3351,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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.7" @@ -2965,6 +3452,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -3086,6 +3584,12 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "x509-parser" version = "0.18.1" @@ -3093,7 +3597,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", - "aws-lc-rs", "data-encoding", "der-parser", "lazy_static", @@ -3101,7 +3604,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", ] @@ -3115,6 +3618,29 @@ dependencies = [ "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.48" @@ -3135,12 +3661,66 @@ dependencies = [ "syn 2.0.117", ] +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index adfe95c..2a8f7d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,25 +19,38 @@ unused = { level = "allow", priority = -1 } [dependencies] # Async -async-trait = "0.1.91" +async-trait = "0.1.92" tokio = { version = "1.53.1", features = ["full"] } +# Arc +arc-swap = "1.9.2" + # Certs # used Ring as backend for default rcgen = { version = "0.14.8", features = ["default"] } # match the backed from rcgen -instant-acme = { version = "0.8.5", features = ["rcgen"] } +instant-acme = { version = "0.8.5", default-features = false, features = [ + "ring", + "rcgen", + "hyper-rustls", +] } +rustls = "0.23.43" + # TOML -toml = "1.1.3" +toml = "1.1.4" # Json serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" # Others -thiserror = "2.0.19" +thiserror = "2.0.20" chrono = { version = "0.4.45", features = ["serde"] } # HTTP -http = "1.4.2" +http = "1.5.0" pingora = { version = "0.8.1", features = ["proxy", "boringssl"] } + +# HTTP Client +reqwest = { version = "0.13.4", features = ["json"] } diff --git a/README.md b/README.md index 7029610..d4d63d7 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ services: proxy: image: maxvanderschee/reverse-proxy:latest # or ghcr.io/mvdschee/reverse-proxy:latest restart: unless-stopped + # host ports 80/443 mapped to container ports 8080/8443 + # see ## Under the hood for explanation ports: - "80:8080" - "443:8443" @@ -100,7 +102,7 @@ The full schema lives in [`example/example.toml`](example/example.toml). The fie | `acme.email` | yes | Contact email for Let's Encrypt (used once ACME lands; required today even if every route is `none`). | | `routes[].host` | yes | The `Host` header to match (e.g. `app.example.com`). | | `routes[].upstream` | yes | `host:port` to forward to. Use `host.docker.internal:` to reach the host machine from Docker. | -| `routes[].cert_type` | no | `self_signed` (default works on boot), `acme` (WIP), or `none` (HTTP only). Defaults to `acme`. | +| `routes[].cert_type` | no | `self_signed` (default works on boot), `acme` (WIP), or `none` (HTTP only). Defaults to `none`. | ## How it works @@ -164,5 +166,12 @@ I'll be upfront on every public project about what was done with AI. For this on - Research and tradeoff discussions - Cleanup of the README and other prose -- Talking through code-level solutions +- Discussing code-level solutions - Generating the Docker image scaffolding from a spec + +# TODO: + +- finish create_acme_dns_challenge +- create a dns entry checker for the background task +- generate a staging certificate with instance_acme +- wire up the full flow in CertBackgroundRenewal diff --git a/docker-compose.yml b/docker-compose.yml index dec2d7b..a289748 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: context: . dockerfile: .docker/Dockerfile ports: + # nonroot user can't bind to ports < 1024, so map to 8080/8443 - "80:8080" - "443:8443" # Lets upstream = "host.docker.internal:" reach apps running diff --git a/example/example.toml b/example/example.toml index 69e6f8a..6eb0a62 100644 --- a/example/example.toml +++ b/example/example.toml @@ -1,12 +1,12 @@ [acme] -email = "max@example.com" +email = "max@example.com" # use a real email this will fail the even on local testing [[routes]] host = "git.example.com" upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker -cert_type = "self_signed" # default is "acme" +cert_type = "self_signed" # default is "none" [[routes]] host = "test.example.com" upstream = "127.0.0.1:3000" # use "host.docker.internal:3000" when using docker -cert_type = "none" # default is "acme" +cert_type = "none" # default is "none" diff --git a/makefile b/makefile index 3cd59f6..b0e05da 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ dev: - CONFIG_PATH=./example/example.toml watchexec -q -c -w src --exts rs --restart "cargo run" + CONFIG_PATH=./example/local.toml watchexec -q -c -w src --exts rs --restart "cargo run" scan: foxguard --config .foxguard.yml diff --git a/src/config.rs b/src/config.rs index 6a6ddde..3669513 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,13 @@ use crate::{ Error, Result, core::models::{ - certs::{CertDir, Email}, + certs::{CertAccountPath, CertDir, Email}, proxy::{ProxyInputAddress, ProxyPort}, routes::Route, tasks::TaskInterval, }, }; +use instant_acme::AccountCredentials; use serde::Deserialize; use std::{env, fs}; @@ -14,19 +15,24 @@ const CONFIG_PATH_ENV: &str = "CONFIG_PATH"; const CERT_DIR_ENV: &str = "CERT_DIR"; const HTTP_PORT_ENV: &str = "HTTP_PORT"; const HTTPS_PORT_ENV: &str = "HTTPS_PORT"; +pub const ACME_CHALLENGE_PREFIX: &str = "_acme-challenge."; const CERT_DIR_DEFAULT: &str = ".certs/"; -const HTTP_PORT_DEFAULT: u16 = 80; -const HTTPS_PORT_DEFAULT: u16 = 443; +// this will be stored in the .certs/ or depending on where the user wants to store it +const CERT_CREDENTIAL_FILE: &str = "acme_account"; +const HTTP_PORT_DEFAULT: u16 = 880; +const HTTPS_PORT_DEFAULT: u16 = 8443; const INPUT_ADDRESS: &str = "0.0.0.0"; -// in seconds -const CERT_BACKGROUND_TASK_INTERVAL: u64 = 120; +/// in seconds +const CERT_BACKGROUND_TASK_INTERVAL: u64 = 3600; // 1 hour #[derive(Debug, Clone)] pub struct Config { pub email: Email, pub cert_dir: CertDir, + // opague string type as it can't be cloned when its in AccountCredentials type + pub cert_account_path: CertAccountPath, pub routes: Vec, pub task_interval: TaskInterval, pub http_port: ProxyPort, @@ -51,6 +57,8 @@ impl Config { let config_file = parse_toml_config(config_path)?; let cert_dir = load_env(CERT_DIR_ENV).unwrap_or_else(|_| CERT_DIR_DEFAULT.to_string()); + let cert_account_path = format!("{}/{}", cert_dir, CERT_CREDENTIAL_FILE); + let http_port = load_env(HTTP_PORT_ENV).ok().and_then(|v| v.parse().ok()).unwrap_or(HTTP_PORT_DEFAULT); let https_port = load_env(HTTPS_PORT_ENV) @@ -61,6 +69,7 @@ impl Config { Ok(Config { email: config_file.acme.email.clone(), cert_dir: CertDir::from(cert_dir), + cert_account_path: CertAccountPath::from(cert_account_path), routes: config_file.routes.clone(), task_interval: TaskInterval::from(CERT_BACKGROUND_TASK_INTERVAL), http_port: ProxyPort::from(http_port), diff --git a/src/core/handlers/certs.rs b/src/core/handlers/certs.rs index f9d088d..ec2980f 100644 --- a/src/core/handlers/certs.rs +++ b/src/core/handlers/certs.rs @@ -1,73 +1,286 @@ use crate::{ Error, Result, core::{ - handlers::filesystem::{safe_path, write_file}, + handlers::filesystem::{check_file_exists, read_file, safe_path, write_file}, models::{ - certs::{CertificateConfig, CertificateType}, + certs::{ + CertAccountPath, CertDir, CertPath, CertificateConfig, CertificateType, Email, + KeyPath, TlsMaterial, TlsStore, + }, + dns::{Cloudflare, CloudflareProvider, DnsProvider, ProviderCredentail}, + routes::Host, tasks::TaskInterval, }, }, - info, + error, info, + services::{ + certs::{ + acme::{create_account, create_order, init_account, load_account}, + self_signed::create_self_signed_certificate_files, + }, + http::create_client, + }, + warn, }; +use arc_swap::ArcSwap; +use async_trait::async_trait; +use instant_acme::{Account, AccountCredentials, Identifier, NewOrder, OrderStatus}; +use pingora::{server::ShutdownWatch, services::background::BackgroundService, tls}; use rcgen::{CertifiedKey, generate_simple_self_signed}; -use std::time::Duration; +use std::{collections::HashMap, fs, sync::Arc, time::Duration}; use tokio::time; -pub fn generate_certs(certificate_configs: Vec) -> Result<()> { - for config in &certificate_configs { - // self signed certificates are good until the year 4096 - // this will be replace every restart so it's safe to keep using the default setting +pub fn create_self_signed_certs(certificate_configs: &Vec) -> Result<()> { + for config in certificate_configs { match config.cert_type { CertificateType::SelfSigned => { - info!("generating self-signed certificate for {}", config.host); + // self signed certificates are good until the year 4096 + // this will be replace every restart so it's safe to keep using the default setting + // for selfsigned we will create the certs here right away + create_self_signed_certificate_files(config); + }, + _ => {}, + } + } - let subject_alt_names = vec![config.host.to_string()]; - let pem_filename = format!("{}.pem", config.host); - let key_filename = format!("{}.key", config.host); + Ok(()) +} - let key_path = safe_path(&config.cert_dir, &key_filename)?; - let pem_path = safe_path(&config.cert_dir, &pem_filename)?; +pub fn load_tls_store(certificate_configs: &Vec) -> Result { + let mut tls_certs = HashMap::new(); - let CertifiedKey { + for config in certificate_configs { + if config.cert_type != CertificateType::None { + let (key_path, cert_path) = certificate_paths(&config.host, &config.cert_dir)?; + + let has_tls_files = check_file_exists(&key_path) && check_file_exists(&cert_path); + + // We only show a warning so its easier to debug once its running, + // but we are not stopping any traffic. + if !has_tls_files { + warn!("Certificate files not found for host '{}' but is expected", &config.host); + continue; + } + + let cert_bytes = read_file(&cert_path)?; + let cert = tls::x509::X509::from_pem(&cert_bytes) + .map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?; + + let key_bytes = read_file(&key_path)?; + let key = tls::pkey::PKey::private_key_from_pem(&key_bytes) + .map_err(|e| Error::Certificate(format!("Failed to parse private key: {}", e)))?; + + tls_certs.insert( + config.host.clone(), + TlsMaterial { cert, - signing_key, - } = generate_simple_self_signed(subject_alt_names) - .map_err(|e| Error::Certificate(e.to_string()))?; + key, + }, + ); + } + } - let pem_serialized = cert.pem(); - let key_serialized = signing_key.serialize_pem(); + let tls_store: TlsStore = Arc::new(ArcSwap::from_pointee(tls_certs)); - write_file(pem_path, pem_serialized.as_bytes())?; - write_file(key_path, key_serialized.as_bytes())?; + Ok(tls_store) +} + +pub fn certificate_paths(host: &Host, cert_dir: &CertDir) -> Result<(KeyPath, CertPath)> { + let cert_filename = format!("{}.pem", host); + let key_filename = format!("{}.key", host); + + let key_path = safe_path(cert_dir, &key_filename)?; + let cert_path = safe_path(cert_dir, &cert_filename)?; + + Ok((key_path, cert_path)) +} + +pub struct CertBackgroundRenewal { + pub certificate_configs: Vec, + pub cert_account_path: CertAccountPath, + pub task_interval: TaskInterval, + pub tls_store: TlsStore, + pub email: Email, +} + +impl CertBackgroundRenewal { + pub fn new( + certificate_configs: Vec, + cert_account_path: CertAccountPath, + task_interval: TaskInterval, + tls_store: TlsStore, + email: Email, + ) -> Self { + Self { + certificate_configs, + cert_account_path, + task_interval, + tls_store, + email, + } + } +} + +#[async_trait] +impl BackgroundService for CertBackgroundRenewal { + // start should never return this will stop the background task, + // this means we have to be a little more verbose with our error handeling. + // TLDR; just continue on any error :D, problem for the next loop :') + async fn start(&self, mut shutdown: ShutdownWatch) { + let http_client = match create_client() { + Ok(client) => client, + Err(e) => { + error!("Can't start cert renewal loop: {}", e); + return; }, - CertificateType::Acme => { - info!("generating acme certificate for {}", config.host); + }; + + let account = match resolve_acme_account(&self.cert_account_path, &self.email).await { + Ok(account) => account, + Err(err) => { + error!("Failed to resolve ACME account: {err:?}"); + return; }, - CertificateType::None => {}, + }; + + let configs = self + .certificate_configs + .clone() + .into_iter() + .filter(|c| c.cert_type == CertificateType::Acme); + + loop { + for config in configs.clone() { + let order_result = create_order(&account, &config.host).await; + + // if no DNS credentials are provided there is DNS validation. + // we should terminate early on. + let dns_service_config = match config.provider_config { + Some(config) => config, + None => { + warn!("No DNS credentials provided for {}", config.host); + continue; + }, + }; + + let dns_service = get_dns_services(dns_service_config); + + let mut order = match order_result { + Ok(order) => order, + Err(err) => { + error!("{err:?}"); + continue; + }, + }; + + let state = order.state(); + info!("order state: {:#?}", state); + + // TODO what does pending means? can we use this to gate the refresh on it. like this will tell us its time to refresh the dns record. + if !matches!(state.status, OrderStatus::Pending) { + warn!("Skipping non-Pending order: {:?}", state.status); + continue; + } + + // TODO dont verify the value go straigh to update we are going to get get_challenge_record once we have put it and use it as a input check before finalizing + + // Pick the desired challenge type and prepare the response. + + // let mut authorizations = order.authorizations(); + // while let Some(result) = authorizations.next().await { + // let mut authz = result?; + // match authz.status { + // AuthorizationStatus::Pending => {}, + // AuthorizationStatus::Valid => continue, + // _ => todo!(), + // } + + // // We'll use the DNS challenges for this example, but you could + // // pick something else to use here. + + // let mut challenge = authz + // .challenge(ChallengeType::Dns01) + // .ok_or_else(|| anyhow::anyhow!("no dns01 challenge found"))?; + + // println!("Please set the following DNS record then press the Return key:"); + // println!( + // "_acme-challenge.{} IN TXT {}", + // challenge.identifier(), + // challenge.key_authorization()?.dns_value() + // ); + // io::stdin().read_line(&mut String::new())?; + + // challenge.set_ready().await?; + // } + + // // Exponentially back off until the order becomes ready or invalid. + + // let status = order.poll_ready(&RetryPolicy::default()).await?; + // if status != OrderStatus::Ready { + // return Err(anyhow::anyhow!("unexpected order status: {status:?}")); + // } + + // // Finalize the order and print certificate chain, private key and account credentials. + + // let private_key_pem = order.finalize().await?; + // let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?; + + // info!("certificate chain:\n\n{cert_chain_pem}"); + // info!("private key:\n\n{private_key_pem}"); + + // write to the file system + // + // swap the file content in the store with the new values if any + // + // + // note: we write to the file system so we can pick the files up and load them in the store when we restart or bootup + // this so we don't have to deal here with loading if the files are there (so we only have to check here if the order is invalid or valid and swap when its time) + // so on boot we load all the tls certs from self-signed / acme and check in this flow it its valid or not and fix it with a swap. + } + + info!("background thing"); + + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(*self.task_interval)) => {} + _ = shutdown.changed() => break, + } } } +} - Ok(()) +fn get_dns_services(config: ProviderCredentail) -> impl DnsProvider { + match config { + ProviderCredentail::Cloudflare(config) => CloudflareProvider { + zone_id: config.zone_id, + api_token: config.api_token, + challenge_prefix: config.challenge_prefix, + }, + } } -pub async fn background_certs_task( - certificates: Vec, - task_interval: TaskInterval, -) -> Result<()> { - // only acme certificates need to be renewed - let certificates = certificates - .into_iter() - .filter(|cert| cert.cert_type == CertificateType::Acme) - .collect::>(); +async fn resolve_acme_account( + cert_account_path: &CertAccountPath, + email: &Email, +) -> Result { + // set crypto lib to load/create the account + init_account(); - loop { - info!("certificates: {}", certificates.len()); + if let Ok(credentials) = get_acme_account(cert_account_path) { + return load_account(credentials).await; + } - // for certificate in &certificates { - // } + let (account, credentials) = create_account(email).await?; + let content = serde_json::to_vec(&credentials) + .map_err(|e| Error::Acme(format!("Failed to serialize ACME account: {}", e)))?; + write_file(cert_account_path.clone(), &content)?; - time::sleep(Duration::from_secs(*task_interval)).await; - } + Ok(account) +} - Ok(()) +// any erorr one this part we will simply create a new account +fn get_acme_account(cert_account_path: &CertAccountPath) -> Result { + let raw_content = read_file(cert_account_path)?; + + serde_json::from_slice(&raw_content) + .map_err(|e| Error::Acme(format!("Failed to parse ACME account: {}", e))) } diff --git a/src/core/handlers/proxy.rs b/src/core/handlers/proxy.rs index 62693f4..a257d6f 100644 --- a/src/core/handlers/proxy.rs +++ b/src/core/handlers/proxy.rs @@ -1,21 +1,22 @@ use crate::{ Error, Result, core::{ - handlers::filesystem::read_file, + handlers::{certs::CertBackgroundRenewal, filesystem::read_file}, models::{ - certs::{TlsCerts, TlsMaterial}, + certs::{TlsMaterial, TlsStore}, proxy::{ProxyConfig, ProxyRoute, ProxyRouteMap}, }, }, error, }; +use arc_swap::ArcSwap; use async_trait::async_trait; use http::header; use pingora::{ ErrorType, http::ResponseHeader, listeners::{TlsAccept, tls::TlsSettings}, - prelude::{Error as PingoraError, HttpPeer, Result as PingoraResult}, + prelude::{Error as PingoraError, HttpPeer, Result as PingoraResult, background_service}, proxy::{ProxyHttp, Session, http_proxy_service}, server::{Server, configuration::ServerConf}, services::Service, @@ -23,7 +24,12 @@ use pingora::{ }; use std::{collections::HashMap, sync::Arc}; -pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<()> { +pub fn run_proxy( + proxy_config: ProxyConfig, + routes: Vec, + tls_store: TlsStore, + renewal: CertBackgroundRenewal, +) -> Result<()> { let mut server = Server::new(None).map_err(|e| Error::Proxy(e.to_string()))?; server.bootstrap(); @@ -31,34 +37,13 @@ pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<( let server_conf = server.configuration.clone(); let http_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.http_port); let https_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.https_port); - let mut routes_map = HashMap::new(); - let mut tls_certs = HashMap::new(); for route in routes { - if *route.tls { - let cert_bytes = read_file(&route.cert_path)?; - let cert = tls::x509::X509::from_pem(&cert_bytes) - .map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?; - - let key_bytes = read_file(&route.key_path)?; - let key = tls::pkey::PKey::private_key_from_pem(&key_bytes) - .map_err(|e| Error::Certificate(format!("Failed to parse private key: {}", e)))?; - - tls_certs.insert( - route.host.clone(), - TlsMaterial { - cert, - key, - }, - ); - } - routes_map.insert(route.host.clone(), route); } let routes_map = Arc::new(routes_map); - let tls_certs = Arc::new(tls_certs); // plain proxies with redirect let plain_service = @@ -66,10 +51,12 @@ pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec) -> Result<( server.add_service(plain_service); // tls proxies - let tls_service = - tls_routes_service(server_conf, https_addr.clone(), routes_map.clone(), tls_certs)?; + let tls_service = tls_routes_service(server_conf, https_addr, routes_map, tls_store)?; server.add_service(tls_service); + // background cert services + server.add_service(background_service("cert-renewal", renewal)); + server.run_forever(); } @@ -77,14 +64,14 @@ pub fn tls_routes_service( server_conf: Arc, listen_addr: String, routes_map: ProxyRouteMap, - tls_certs: TlsCerts, + tls_store: TlsStore, ) -> Result { let proxy_app = ProxyToUpstream::new(routes_map.clone(), false); let mut service = http_proxy_service(&server_conf, proxy_app); - let sni_resolver = SniResolver::new(tls_certs); - let callback = Box::new(sni_resolver); + let cert_resolver = CertResolver::new(tls_store); + let callback = Box::new(cert_resolver); let tls_settings = TlsSettings::with_callbacks(callback).map_err(|e| Error::Proxy(e.to_string()))?; service.add_tls_with_settings(&listen_addr, None, tls_settings); @@ -186,20 +173,20 @@ fn host_from_session(session: &Session) -> Option<&str> { session.get_header(header::HOST).and_then(|h| h.to_str().ok()) } -struct SniResolver { - certs: TlsCerts, +struct CertResolver { + tls_store: TlsStore, } -impl SniResolver { - fn new(tls_certs: TlsCerts) -> Self { +impl CertResolver { + fn new(tls_store: TlsStore) -> Self { Self { - certs: tls_certs, + tls_store, } } } #[async_trait] -impl TlsAccept for SniResolver { +impl TlsAccept for CertResolver { async fn certificate_callback(&self, ssl: &mut ssl::SslRef) -> () { let sni_provided = ssl.servername(ssl::NameType::HOST_NAME).map(str::to_owned); @@ -208,10 +195,12 @@ impl TlsAccept for SniResolver { return; }; + let certs = self.tls_store.load(); + let Some(TlsMaterial { cert, key, - }) = self.certs.get(sni_provided.as_str()) + }) = certs.get(sni_provided.as_str()) else { error!("No certificate found for SNI: {}", sni_provided); return; diff --git a/src/core/models/certs.rs b/src/core/models/certs.rs index 818149a..4ad5470 100644 --- a/src/core/models/certs.rs +++ b/src/core/models/certs.rs @@ -1,20 +1,21 @@ -use crate::core::models::routes::Host; +use crate::{ + core::models::{dns::ProviderCredentail, filesystem::SafePath, routes::Host}, + string_newtype, +}; +use arc_swap::ArcSwap; use pingora::tls::{ pkey::{PKey, Private}, x509::X509, }; use serde::Deserialize; -use std::collections::HashMap; -use std::fmt; -use std::ops::Deref; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, Deserialize, Default, PartialEq)] #[serde(rename_all = "snake_case")] pub enum CertificateType { SelfSigned, - #[default] Acme, + #[default] None, } @@ -22,8 +23,8 @@ pub enum CertificateType { pub struct CertificateConfig { pub host: Host, pub cert_dir: CertDir, - pub email: Email, pub cert_type: CertificateType, + pub provider_config: Option, } pub struct TlsMaterial { @@ -31,52 +32,16 @@ pub struct TlsMaterial { pub key: PKey, } -pub type TlsCerts = Arc>; - -// --- EMAIL --- -#[derive(Debug, Clone, Deserialize)] -pub struct Email(String); +pub type KeyPath = SafePath; +pub type CertPath = SafePath; -impl Deref for Email { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for Email { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} +pub type TlsStore = Arc>>; -impl From for Email { - fn from(s: String) -> Self { - Email(s) - } -} +// --- EMAIL --- +string_newtype!(Email, derive(Deserialize)); // --- CERT_DIR --- -#[derive(Debug, Clone, Deserialize)] -pub struct CertDir(String); - -impl Deref for CertDir { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for CertDir { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} +string_newtype!(CertDir, derive(Deserialize)); -impl From for CertDir { - fn from(s: String) -> Self { - CertDir(s) - } -} +// --- CERT_ACCOUNT_PATH --- +pub type CertAccountPath = SafePath; diff --git a/src/core/models/dns.rs b/src/core/models/dns.rs new file mode 100644 index 0000000..e16a458 --- /dev/null +++ b/src/core/models/dns.rs @@ -0,0 +1,59 @@ +use crate::{ + Error, Result, config::ACME_CHALLENGE_PREFIX, core::models::routes::Host, string_newtype, +}; +use reqwest::Client; +use serde::Deserialize; + +pub trait DnsProvider: Send + Sync + 'static { + /// Only returns single record + /// gets all records but internally filters out all records either, + /// after the call or if it supports it directly on the endpoint call. + fn get_challenge_record(&self, host: &Host) -> Result; + /// Single fuction to set and update + /// will return the set record (if returned otherwise fake it) + fn update_challenge_record(&self, host: &Host, value: &str) -> Result; +} + +fn default_challenge_prefix() -> String { + ACME_CHALLENGE_PREFIX.to_string() +} + +// --- DNS Record --- +string_newtype!(Record, derive(Deserialize)); + +// --- DNS Provider credentials --- + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProviderCredentail { + Cloudflare(CloudflareProvider), +} + +// --- add here any other providers credentials --- + +// --- CloudFlare --- + +// config coming from TOML file +#[derive(Debug, Clone, Deserialize)] +pub struct CloudflareProvider { + pub zone_id: ZoneId, + pub api_token: ApiToken, + // challenge_prefix is a default value from ACME, + // this can not be set or changed from the userside + // but its here to keep keep it as a single config + // per provider + #[serde(skip, default = "default_challenge_prefix")] + pub challenge_prefix: String, +} + +// struct that hold client +pub struct Cloudflare { + client: Client, + config: CloudflareProvider, +} + +// Zone ID +string_newtype!(ZoneId, derive(Deserialize)); + +// API token +string_newtype!(ApiToken, derive(Deserialize)); diff --git a/src/core/models/filesystem.rs b/src/core/models/filesystem.rs index 53c4570..3a70b54 100644 --- a/src/core/models/filesystem.rs +++ b/src/core/models/filesystem.rs @@ -1,25 +1,4 @@ -use std::{fmt, ops::Deref}; +use crate::string_newtype; // --- SAFEPATH --- -#[derive(Debug, Clone)] -pub struct SafePath(String); - -impl Deref for SafePath { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for SafePath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for SafePath { - fn from(s: String) -> Self { - SafePath(s) - } -} +string_newtype!(SafePath); diff --git a/src/core/models/mod.rs b/src/core/models/mod.rs index 1cf9ecb..7052b71 100644 --- a/src/core/models/mod.rs +++ b/src/core/models/mod.rs @@ -1,4 +1,5 @@ pub mod certs; +pub mod dns; pub mod filesystem; pub mod proxy; pub mod routes; diff --git a/src/core/models/proxy.rs b/src/core/models/proxy.rs index dab8d40..7db4914 100644 --- a/src/core/models/proxy.rs +++ b/src/core/models/proxy.rs @@ -1,10 +1,13 @@ -use crate::core::models::{ - certs::CertDir, - filesystem::SafePath, - routes::{Host, Upstream}, +use crate::{ + core::models::{ + certs::CertDir, + filesystem::SafePath, + routes::{Host, Upstream}, + }, + string_newtype, }; use http::{Response, StatusCode, header}; -use std::{collections::HashMap, fmt, ops::Deref, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; pub type ProxyRouteMap = Arc>; @@ -13,13 +16,10 @@ pub struct ProxyRoute { pub host: Host, pub upstream: Upstream, pub tls: ProxyTls, - pub cert_path: SafePath, - pub key_path: SafePath, } #[derive(Debug, Clone)] pub struct ProxyConfig { - pub cert_dir: CertDir, pub http_port: ProxyPort, pub https_port: ProxyPort, pub input_address: ProxyInputAddress, @@ -62,25 +62,4 @@ impl From for ProxyPort { } // --- PROXY INPUT ADDRESS --- -#[derive(Debug, Clone)] -pub struct ProxyInputAddress(String); - -impl Deref for ProxyInputAddress { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for ProxyInputAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for ProxyInputAddress { - fn from(s: String) -> Self { - ProxyInputAddress(s) - } -} +string_newtype!(ProxyInputAddress); diff --git a/src/core/models/routes.rs b/src/core/models/routes.rs index 17649ec..b44a2e3 100644 --- a/src/core/models/routes.rs +++ b/src/core/models/routes.rs @@ -1,6 +1,8 @@ -use crate::core::models::certs::CertificateType; +use crate::{ + core::models::{certs::CertificateType, dns::ProviderCredentail}, + string_newtype, +}; use serde::Deserialize; -use std::{fmt, ops::Deref}; #[derive(Debug, Clone, Deserialize)] pub struct Route { @@ -8,19 +10,11 @@ pub struct Route { pub upstream: Upstream, #[serde(default)] pub cert_type: CertificateType, + pub dns_provider: Option, } // --- HOST --- -#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)] -pub struct Host(String); - -impl Deref for Host { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} +string_newtype!(Host, derive(Deserialize, Hash, Eq, PartialEq)); impl std::borrow::Borrow for Host { fn borrow(&self) -> &str { @@ -28,38 +22,5 @@ impl std::borrow::Borrow for Host { } } -impl fmt::Display for Host { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Host { - fn from(s: String) -> Self { - Host(s) - } -} - // --- UPSTREAM --- -#[derive(Debug, Clone, Deserialize)] -pub struct Upstream(String); - -impl Deref for Upstream { - type Target = String; - - fn deref(&self) -> &String { - &self.0 - } -} - -impl fmt::Display for Upstream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Upstream { - fn from(s: String) -> Self { - Upstream(s) - } -} +string_newtype!(Upstream, derive(Deserialize)); diff --git a/src/core/models/tasks.rs b/src/core/models/tasks.rs index 1cc2b46..8722ed1 100644 --- a/src/core/models/tasks.rs +++ b/src/core/models/tasks.rs @@ -2,6 +2,7 @@ use serde::Deserialize; use std::ops::Deref; // --- TASK INTERVAL --- +/// Interval for a task in seconds. #[derive(Debug, Clone, Deserialize)] pub struct TaskInterval(u64); diff --git a/src/core/setup.rs b/src/core/setup.rs index b133345..4be3f61 100644 --- a/src/core/setup.rs +++ b/src/core/setup.rs @@ -2,12 +2,16 @@ use crate::{ Error, Result, core::{ handlers::{ - certs::generate_certs, + certs::{ + CertBackgroundRenewal, certificate_paths, create_self_signed_certs, load_tls_store, + }, filesystem::{check_file_exists, safe_path}, proxy::run_proxy, }, models::{ - certs::{CertDir, CertificateConfig, CertificateType, Email}, + certs::{ + CertAccountPath, CertDir, CertificateConfig, CertificateType, Email, TlsStore, + }, proxy::{ProxyConfig, ProxyRoute, ProxyTls}, routes::Route, tasks::TaskInterval, @@ -46,12 +50,15 @@ impl HandleFileSystem { pub struct HandleCertificates { certificate_configs: Vec, + cert_account_path: CertAccountPath, task_interval: TaskInterval, + email: Email, } impl HandleCertificates { pub fn new( cert_dir: CertDir, + cert_account_path: CertAccountPath, email: Email, routes: Vec, task_interval: TaskInterval, @@ -61,72 +68,69 @@ impl HandleCertificates { .map(|route| CertificateConfig { host: route.host.clone(), cert_dir: cert_dir.clone(), - email: email.clone(), cert_type: route.cert_type.clone(), + provider_config: route.dns_provider.clone(), }) .collect::>(); Self { certificate_configs, + cert_account_path, task_interval, + email, } } - pub fn run(self) -> Result<()> { - generate_certs(self.certificate_configs.clone())?; + pub fn run(self) -> Result<(TlsStore, CertBackgroundRenewal)> { + create_self_signed_certs(&self.certificate_configs)?; - // spawn(async move { - // info!("starting certificates tasks..."); - // background_certs_task(self.certificates, self.task_interval).await; - // }); + let store = load_tls_store(&self.certificate_configs)?; + let renewal = CertBackgroundRenewal::new( + self.certificate_configs.clone(), + self.cert_account_path.clone(), + self.task_interval.clone(), + store.clone(), + self.email.clone(), + ); - Ok(()) + Ok((store, renewal)) } } pub struct HandleProxy { proxy_config: ProxyConfig, proxy_routes: Vec, + tls_store: TlsStore, } impl HandleProxy { - pub fn new(proxy_config: ProxyConfig, routes: Vec) -> Result { + pub fn new(proxy_config: ProxyConfig, routes: Vec, tls_store: TlsStore) -> Result { let mut proxy_routes = Vec::new(); for route in routes { - let cert_filename = format!("{}.pem", route.host); - let key_filename = format!("{}.key", route.host); - - let key_path = safe_path(&proxy_config.cert_dir, &key_filename)?; - let cert_path = safe_path(&proxy_config.cert_dir, &cert_filename)?; - - let has_tls_files = check_file_exists(&key_path) && check_file_exists(&cert_path); - - // We only show a warning so its easier to debug once its running, - // but we are not stopping any traffic. - if !has_tls_files && route.cert_type != CertificateType::None { - warn!("Certificate files not found for host `{}` but is expected", route.host); - } - proxy_routes.push(ProxyRoute { host: route.host.clone(), upstream: route.upstream.clone(), tls: ProxyTls::from(route.cert_type != CertificateType::None), - cert_path, - key_path, }); } Ok(Self { proxy_config, proxy_routes, + tls_store, }) } - pub fn run(&self) -> Result<()> { + pub fn run(&self, renewal: CertBackgroundRenewal) -> Result<()> { info!("proxy running..."); - run_proxy(self.proxy_config.clone(), self.proxy_routes.clone())?; + run_proxy( + self.proxy_config.clone(), + self.proxy_routes.clone(), + self.tls_store.clone(), + renewal, + )?; Err(Error::Proxy("proxy exited".to_string())) } diff --git a/src/error.rs b/src/error.rs index a9f79ac..10da973 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,18 +13,33 @@ pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), + #[error("request error: {0}")] + Request(reqwest::Error), + #[error("file system error: {0}")] FileSystem(String), #[error("certificate error: {0}")] Certificate(String), + #[error("Acme error: {0}")] + Acme(String), + #[error("proxy error: {0}")] Proxy(String), + #[error("dns error: {0}")] + Dns(String), + #[error("config error: {0}")] Config(String), #[error("unknown error")] Unknown, } + +impl From for Error { + fn from(err: reqwest::Error) -> Self { + Error::Request(err) + } +} diff --git a/src/main.rs b/src/main.rs index ad61be0..1a54adc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ pub use error::{Error, Result}; mod config; mod core; pub mod error; +mod services; mod utils; // entry needs to be synchronous as pingora has there own @@ -25,21 +26,21 @@ fn main() -> Result<()> { // and spin up background tasks to refresh certificates let cert_handler = HandleCertificates::new( config.cert_dir.clone(), + config.cert_account_path.clone(), config.email, config.routes.clone(), config.task_interval, ); - cert_handler.run()?; + let (store, renewal) = cert_handler.run()?; // start the proxy let proxy_config = ProxyConfig { - cert_dir: config.cert_dir.clone(), http_port: config.http_port, https_port: config.https_port, input_address: config.input_address, }; - let proxy_handler = HandleProxy::new(proxy_config, config.routes)?; - proxy_handler.run()?; + let proxy_handler = HandleProxy::new(proxy_config, config.routes, store)?; + proxy_handler.run(renewal)?; Err(Error::MainLoopClosed) } diff --git a/src/services/certs/acme.rs b/src/services/certs/acme.rs new file mode 100644 index 0000000..4aadaf4 --- /dev/null +++ b/src/services/certs/acme.rs @@ -0,0 +1,67 @@ +use crate::{ + Error, Result, + core::models::{ + certs::{CertificateConfig, Email}, + routes::Host, + }, + info, +}; +use instant_acme::{ + Account, AccountCredentials, Identifier, LetsEncrypt, NewAccount, NewOrder, Order, +}; +use rustls::crypto::CryptoProvider; + +pub fn init_account() { + CryptoProvider::install_default(rustls::crypto::ring::default_provider()); +} + +pub async fn load_account(credentials: AccountCredentials) -> Result { + let account = Account::builder() + .map_err(|e| Error::Acme(e.to_string()))? + .from_credentials(credentials) + .await + .map_err(|e| Error::Acme(e.to_string()))?; + + info!("Reusing ACME account"); + + Ok(account) +} + +// this one returns credentials, because we are going to save it to the file +// for later usage to start the process with the same credentials +pub async fn create_account(email: &Email) -> Result<(Account, AccountCredentials)> { + CryptoProvider::install_default(rustls::crypto::ring::default_provider()); + + let (account, credentials) = Account::builder() + .map_err(|e| Error::Acme(e.to_string()))? + .create( + &NewAccount { + contact: &[&format!("mailto:{}", email.as_str())], + terms_of_service_agreed: true, + only_return_existing: false, + }, + // TODO: switch to prod + LetsEncrypt::Staging.url().to_owned(), + None, + ) + .await + .map_err(|e| Error::Acme(e.to_string()))?; + + info!("Created new ACME account"); + + Ok((account, credentials)) +} + +pub async fn create_order(account: &Account, host: &Host) -> Result { + let identifier = Identifier::Dns(host.to_string()); + // instant_acme support multiple host per order, + // but we need to know which dns needs to be updated + // which is why we split them up, this is maybe something we can refine in V2 + let identifiers = vec![identifier]; + let order_result = account + .new_order(&NewOrder::new(&identifiers)) + .await + .map_err(|e| Error::Acme(format!("Failed with new_order: {}", e))); + + order_result +} diff --git a/src/services/certs/mod.rs b/src/services/certs/mod.rs new file mode 100644 index 0000000..7c4495a --- /dev/null +++ b/src/services/certs/mod.rs @@ -0,0 +1,2 @@ +pub mod acme; +pub mod self_signed; diff --git a/src/services/certs/self_signed.rs b/src/services/certs/self_signed.rs new file mode 100644 index 0000000..21099bf --- /dev/null +++ b/src/services/certs/self_signed.rs @@ -0,0 +1,34 @@ +use crate::{ + Error, Result, + core::{ + handlers::{ + certs::certificate_paths, + filesystem::{safe_path, write_file}, + }, + models::certs::CertificateConfig, + }, + info, +}; +use rcgen::{CertifiedKey, generate_simple_self_signed}; + +pub fn create_self_signed_certificate_files(config: &CertificateConfig) -> Result<()> { + info!("generating self-signed certificate for {}", config.host); + + let subject_alt_names = vec![config.host.to_string()]; + + let (key_path, cert_path) = certificate_paths(&config.host, &config.cert_dir)?; + + let CertifiedKey { + cert, + signing_key, + } = generate_simple_self_signed(subject_alt_names) + .map_err(|e| Error::Certificate(e.to_string()))?; + + let cert_serialized = cert.pem(); + let key_serialized = signing_key.serialize_pem(); + + write_file(cert_path, cert_serialized.as_bytes())?; + write_file(key_path, key_serialized.as_bytes())?; + + Ok(()) +} diff --git a/src/services/dns/cloudflare.rs b/src/services/dns/cloudflare.rs new file mode 100644 index 0000000..b41363a --- /dev/null +++ b/src/services/dns/cloudflare.rs @@ -0,0 +1,18 @@ +use serde::de::value; + +use crate::{ + Error, Result, + core::models::{ + dns::{CloudflareProvider, DnsProvider, Record}, + routes::Host, + }, +}; + +impl DnsProvider for CloudflareProvider { + fn get_challenge_record(&self, host: &Host) -> Result { + Err(Error::Dns("not implemented!".to_string())) + } + fn update_challenge_record(&self, host: &Host, value: &str) -> Result { + Err(Error::Dns("not implemented!".to_string())) + } +} diff --git a/src/services/dns/mod.rs b/src/services/dns/mod.rs new file mode 100644 index 0000000..c727ae5 --- /dev/null +++ b/src/services/dns/mod.rs @@ -0,0 +1 @@ +mod cloudflare; diff --git a/src/services/http/mod.rs b/src/services/http/mod.rs new file mode 100644 index 0000000..731029c --- /dev/null +++ b/src/services/http/mod.rs @@ -0,0 +1,15 @@ +use crate::Result; +use reqwest::Client; +use std::time::Duration; + +// simply return client no need for struct, +// or extras +pub fn create_client() -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .pool_idle_timeout(Duration::from_secs(90)) + .pool_max_idle_per_host(10) + .build()?; + + Ok(client) +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..cc7959b --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,3 @@ +pub mod certs; +pub mod dns; +pub mod http; diff --git a/src/utils.rs b/src/utils.rs index b253deb..4225ce5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,3 +18,30 @@ macro_rules! error { println!("\x1b[90m{} \x1b[31m{} \x1b[0m{}", chrono::Local::now().format("%H:%M:%S%.3f %d-%m-%y"), "[ERROR]", format_args!($($arg)*)) }; } + +#[macro_export] +macro_rules! string_newtype { + ($name:ident $(, derive($($extra:path),+ $(,)?))?) => { + #[derive(Debug, Clone $(, $($extra),+)?)] + pub struct $name(String); + + impl ::std::ops::Deref for $name { + type Target = String; + fn deref(&self) -> &String { + &self.0 + } + } + + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl From for $name { + fn from(s: String) -> Self { + $name(s) + } + } + }; +}