From 40cd0021b22f9b30b94c29a3e2a3f3fc10b2bb11 Mon Sep 17 00:00:00 2001 From: Samuel Bachmann Date: Sun, 2 Apr 2023 11:37:30 +0200 Subject: [PATCH] initial commit --- .dockerignore | 2 + .github/workflows/ci.yml | 31 + .gitignore | 2 + Cargo.lock | 2121 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 24 + Dockerfile | 30 + README.md | 64 ++ src/bouncer.rs | 256 +++++ src/config.rs | 102 ++ src/constants.rs | 4 + src/crowdsec.rs | 246 +++++ src/crowdsec/tests.rs | 174 ++++ src/errors.rs | 11 + src/main.rs | 83 ++ src/types.rs | 34 + src/utils.rs | 66 ++ src/utils/tests.rs | 50 + 17 files changed, 3300 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 src/bouncer.rs create mode 100644 src/config.rs create mode 100644 src/constants.rs create mode 100644 src/crowdsec.rs create mode 100644 src/crowdsec/tests.rs create mode 100644 src/errors.rs create mode 100644 src/main.rs create mode 100644 src/types.rs create mode 100644 src/utils.rs create mode 100644 src/utils/tests.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6603685 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/.github +/target diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3e085e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: ci + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy, rustfmt + - name: Build + run: cargo build --release --all-features --verbose + - name: Run tests + run: cargo test --verbose + - name: Run clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + if: github.event_name == 'pull_request' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fe9b98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +docker-compose.yml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..727b1f5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2121 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-http" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.3", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.20", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.20", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "custom_error" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" + +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.12", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "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 = "iana-time-zone" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "ip_network_table-deps-treebitmap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[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 = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "mockito" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea57936ab3bf56156f135f20ee24b840e5a8ad97a8e1710eace33ac078f8f537" +dependencies = [ + "assert-json-diff", + "colored", + "futures", + "hyper", + "lazy_static", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "openssl" +version = "0.10.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d2f106ab837a24e03672c59b1239669a0596406ff657c3c0835b6b7f0f35a33" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a20eace9dc2d82904039cb76dcf50fb1a0bba071cfd1629720b5d6f1ddba0fa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "parse_duration" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d" +dependencies = [ + "lazy_static", + "num", + "regex", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +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 = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "security-framework" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[package]] +name = "serde_json" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +dependencies = [ + "itoa", + "ryu", + "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 = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.45.0", +] + +[[package]] +name = "tokio-macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + +[[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]] +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", + "log", + "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 = "traefik_crowdsec_bouncer" +version = "0.2.1" +dependencies = [ + "actix-rt", + "actix-web", + "chrono", + "custom_error", + "env_logger", + "ip_network_table-deps-treebitmap", + "log", + "mockito", + "parse_duration", + "reqwest", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "6.0.4+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.7+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c8da58c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "traefik_crowdsec_bouncer" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4" +actix-rt = "2" +chrono = { version = "0.4", features = ["time"] } +custom_error = "1.9" +env_logger = "0.10" +ip_network_table-deps-treebitmap = "0.5" +log = "0.4" +parse_duration = "2" +reqwest = { version = "0.11", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +url = "2.3" + +[dev-dependencies] +mockito = "1.0.2" +tokio = { version = "1", features = ["full"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3f9e34 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM rust:1.68 as builder + +# Make use of cache for dependencies. +RUN USER=root cargo new --bin traefik_crowdsec_bouncer +WORKDIR ./traefik_crowdsec_bouncer +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml +RUN cargo build --release && \ + rm src/*.rs + +# Build the app. +COPY . ./ +RUN rm ./target/release/deps/traefik_crowdsec_bouncer* +RUN cargo build --release + + +# Use distroless as minimal base image to package the app. +FROM gcr.io/distroless/cc-debian11:nonroot + +COPY --from=builder --chown=nonroot:nonroot /traefik_crowdsec_bouncer/target/release/traefik_crowdsec_bouncer /app/traefik_crowdsec_bouncer +COPY --from=samuelba/healthcheck:latest --chown=nonroot:nonroot /app/healthcheck /app/healthcheck +USER nonroot +WORKDIR /app +EXPOSE 9090 + +ENV PORT=9090 +ENV API_PATH=api/v1/health +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s CMD ["/app/healthcheck"] + +CMD ["./traefik_crowdsec_bouncer"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1502dad --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +![GitHub](https://img.shields.io/github/license/samuelba/traefik_crowdsec_bouncer) +[![ci](https://github.com/samuelba/traefik_crowdsec_bouncer/actions/workflows/ci.yml/badge.svg)](https://github.com/samuelba/traefik_crowdsec_bouncer/actions/workflows/ci.yml) +![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/samuelba/traefik_crowdsec_bouncer) +![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/samuelba/traefik_crowdsec_bouncer) + +# Traefik CrowdSec Bouncer + +This projects implements a simple HTTP server that acts as a bouncer for the Traefik ForwardAuth middleware. +It uses the CrowdSec API to check if the IP address of the request is banned or not. + +The service support three modes "stream", "live" and "none". +The mode can be configured with the `CROWDSEC_MODE` environment variable. + +| Mode | Description | +|---------|---------------------------------------------------------------------------------------------------------------------| +| stream | Periodically, every `STREAM_UPDATE_INTERVAL` seconds, fetch the list of blocked IP addresses from the CrowdSec API. | +| live | Call the CrowdSec API for every unknown IP address and store it for `LIVE_CACHE_EXPIRATION` seconds in the cache. | +| none | Call the CrowdSec API for every request (not very resource friendly). | + +## Usage + +### Docker Compose + +Example `docker-compose.yml` file + +```yaml +version: '3.7' + +services: + traefik-crowdsec-bouncer: + # Build the image locally. + build: + context: . + dockerfile: Dockerfile + image: traefik-crowdsec-bouncer:latest + + # Use the image from Docker Hub. + # image: samuelba/traefik_crowdsec_bouncer:latest + + container_name: traefik-crowdsec-bouncers + restart: unless-stopped + environment: + # CrowdSec API key. Get it with e.g. `docker exec -it crowdsec cscli bouncers add traefik-crowdsec-bouncer`. + - CROWDSEC_API_KEY=abc123 + # CrowdSec API host including the port e.g. crowdsec:8080. + - CROWDSEC_HOST=crowdsec:8080 + # Call CrowdSec API over HTTPS (true) or HTTP (false). + - CROWDSEC_HTTPS=false + # The mode to verify the IP address. Can be either "stream", "live" or "none". + # In "stream" mode the service will periodically fetch the list of blocked IP addresses from the CrowdSec API. + # In "live" mode the service will call the CrowdSec API for every unknown IP address and store it for 'LIVE_CACHE_EXPIRATION' seconds in the cache. + # In "none" mode the service will call the CrowdSec API for every request. Not very resource friendly. + - CROWDSEC_MODE=stream + # Stream update interval in seconds. Only needed in "stream" mode. + - STREAM_UPDATE_INTERVAL=5 + # The cache expiration time in seconds. Only needed in "live" mode. + - LIVE_CACHE_EXPIRATION=5 +``` + +## Roadmap + +* [ ] Add support for log level. +* [ ] Add support for trusted proxies. +* [ ] Add support for different server listening port. diff --git a/src/bouncer.rs b/src/bouncer.rs new file mode 100644 index 0000000..36997ae --- /dev/null +++ b/src/bouncer.rs @@ -0,0 +1,256 @@ +use std::cmp::min; +use std::net::Ipv4Addr; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +use log::{info, warn}; + +use actix_web::web::Data; +use actix_web::{HttpRequest, HttpResponse}; + +use ip_network_table_deps_treebitmap::IpLookupTable; + +use parse_duration::parse; + +use crate::config::{Config, CrowdSecMode}; +use crate::constants::{APPLICATION_JSON, TEXT_PLAIN}; +use crate::crowdsec::get_decision; +use crate::types::{CacheAttributes, HealthStatus}; + +fn forbidden_response(ip: Option) -> HttpResponse { + if let Some(ip) = ip { + info!("IP: {} is not allowed", ip); + } + HttpResponse::Forbidden() + .content_type(TEXT_PLAIN) + .body("Forbidden") +} + +fn allowed_response(ip: Option) -> HttpResponse { + if let Some(ip) = ip { + info!("IP: {} is allowed", ip); + } + HttpResponse::Ok().finish() +} + +fn set_health_status(health_status: Arc>, healthy: bool) { + if let Ok(mut health_status) = health_status.lock() { + health_status.live_status = healthy; + } +} + +/// Authenticate an IP address. +/// # Arguments +/// * `config` - The configuration. +/// * `health_status` - The health status. +/// * `ipv4_data` - The IPv4 lookup table. +/// * `request` - The HTTP request. +/// # Returns +/// * `HttpResponse` - The HTTP response. Either `Ok` or `Forbidden`. +#[get("/api/v1/forwardAuth")] +pub async fn authenticate( + config: Data, + health_status: Data>>, + ipv4_data: Data>>>, + request: HttpRequest, +) -> HttpResponse { + // Get the IP address from the X-Forwarded-For header. + let req_ip_str = if let Some(header_value) = request.headers().get("X-Forwarded-For") { + if let Ok(header_value) = header_value.to_str() { + header_value.to_string() + } else { + warn!("Could not convert 'X-Forwarded-For' to string. Block request."); + return forbidden_response(None); + } + } else { + warn!("No 'X-Forwarded-For' in the request header. Block request."); + return forbidden_response(None); + }; + + match config.crowdsec_mode { + CrowdSecMode::Stream => { + return if let Ok(ipv4_table) = ipv4_data.lock() { + let req_ip = Ipv4Addr::from_str(&req_ip_str); + match req_ip { + Ok(ip) => { + if ipv4_table.exact_match(ip, 32).is_some() { + forbidden_response(Some(req_ip_str)) + } else { + allowed_response(Some(req_ip_str)) + } + } + Err(_) => forbidden_response(Some(req_ip_str)), + } + } else { + warn!("Could not lock the IPv4 lookup table. Block request."); + forbidden_response(Some(req_ip_str)) + } + } + CrowdSecMode::Live => { + let req_ip = Ipv4Addr::from_str(&req_ip_str); + match req_ip { + Ok(ip) => { + // Check if IP is in cache. + // If yes, check if it is expired. + // If not, return the cached value. + if let Ok(ipv4_table) = ipv4_data.lock() { + if let Some(cache_attributes) = ipv4_table.exact_match(ip, 32) { + if cache_attributes.expiration_time + > chrono::Utc::now().timestamp_millis() + { + return if cache_attributes.allowed { + allowed_response(Some(req_ip_str)) + } else { + forbidden_response(Some(req_ip_str)) + }; + } + } + } + + // IP not in cache or expired. + // Call CrowdSec API. + // Update cache. + return match get_decision( + &config.crowdsec_live_url, + &config.crowdsec_api_key, + &req_ip_str, + ) + .await + { + Ok(decision) => { + set_health_status(health_status.get_ref().clone(), true); + match decision { + Some(decision) => { + // If the decisions duration is smaller than the cache TTL, use it instead. + let ttl = if let Ok(duration) = parse(&decision.duration) { + min(duration.as_millis() as i64, config.crowdsec_cache_ttl) + } else { + config.crowdsec_cache_ttl + }; + + // Update cache. + if let Ok(mut ipv4_table) = ipv4_data.lock() { + ipv4_table.insert( + ip, + 32, + CacheAttributes { + allowed: false, + expiration_time: chrono::Utc::now() + .timestamp_millis() + + ttl, + }, + ); + } + forbidden_response(Some(req_ip_str)) + } + None => { + // Update cache. + if let Ok(mut ipv4_table) = ipv4_data.lock() { + ipv4_table.insert( + ip, + 32, + CacheAttributes { + allowed: true, + expiration_time: chrono::Utc::now() + .timestamp_millis() + + config.crowdsec_cache_ttl, + }, + ); + } + allowed_response(Some(req_ip_str)) + } + } + } + Err(err) => { + info!( + "Could not call API. IP: {} is not allowed. Error {}", + req_ip_str, err + ); + set_health_status(health_status.get_ref().clone(), false); + forbidden_response(None) + } + }; + } + Err(_) => forbidden_response(Some(req_ip_str)), + } + } + CrowdSecMode::None => { + match get_decision( + &config.crowdsec_live_url, + &config.crowdsec_api_key, + &req_ip_str, + ) + .await + { + Ok(decision) => { + set_health_status(health_status.get_ref().clone(), true); + match decision { + Some(_) => forbidden_response(Some(req_ip_str)), + None => allowed_response(Some(req_ip_str)), + } + } + Err(err) => { + info!( + "Could not call API. IP: {} is not allowed. Error {}", + req_ip_str, err + ); + set_health_status(health_status.get_ref().clone(), false); + forbidden_response(None) + } + } + } + } +} + +/// Get the list of blocked IP addresses. This is only available in stream mode. +/// # Arguments +/// * `config` - The configuration. +/// * `ipv4_data` - The IPv4 lookup table. +/// # Returns +/// * `HttpResponse` - The HTTP response. +#[get("/api/v1/blockList")] +pub async fn block_list( + config: Data, + ipv4_data: Data>>>, +) -> HttpResponse { + match config.crowdsec_mode { + CrowdSecMode::Stream => { + if let Ok(ipv4_table) = ipv4_data.lock() { + let mut list: Vec = Vec::new(); + let iter = ipv4_table.iter(); + for (ip, _, _) in iter { + list.push(format!("{}", ip)); + } + return HttpResponse::Ok() + .content_type(APPLICATION_JSON) + .json(&list); + } + HttpResponse::InternalServerError() + .content_type(TEXT_PLAIN) + .body("Could not generate the block list.") + } + CrowdSecMode::Live => HttpResponse::Ok() + .content_type(TEXT_PLAIN) + .body("Only available in stream mode."), + CrowdSecMode::None => HttpResponse::Ok() + .content_type(TEXT_PLAIN) + .body("Only available in stream mode."), + } +} + +/// Get the health status of the service. +/// # Arguments +/// * `health_status` - The health status. +/// # Returns +/// * `HttpResponse` - The HTTP response. +#[get("/api/v1/health")] +pub async fn health(health_status: Data>>) -> HttpResponse { + if let Ok(health_status) = health_status.lock() { + if health_status.healthy() { + return HttpResponse::Ok().content_type(TEXT_PLAIN).body("OK"); + } + } + HttpResponse::ServiceUnavailable() + .content_type(TEXT_PLAIN) + .body("NOT OK") +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d68e3a0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,102 @@ +use std::env; + +use crate::constants::{CROWDSEC_LIVE_ROUTE, CROWDSEC_STREAM_ROUTE}; + +#[derive(Clone)] +pub enum CrowdSecMode { + Live, + None, + Stream, +} + +#[derive(Clone)] +pub struct Config { + /// The CrowdSec live url. + pub crowdsec_live_url: String, + /// The CrowdSec stream url. + pub crowdsec_stream_url: String, + /// The CrowdSec API key. + pub crowdsec_api_key: String, + /// The CrowdSec mode. + pub crowdsec_mode: CrowdSecMode, + /// The cache expiration time in milliseconds. + pub crowdsec_cache_ttl: i64, + /// The CrowdSec stream update interval in seconds. + pub stream_interval: u64, +} + +/// Read the configuration from the environment variables. +/// # Returns +/// The configuration. +pub fn read_config() -> Config { + let mut config = Config { + crowdsec_live_url: String::new(), + crowdsec_stream_url: String::new(), + crowdsec_api_key: String::new(), + crowdsec_mode: CrowdSecMode::Stream, + crowdsec_cache_ttl: 5000, + stream_interval: 0, + }; + + // Get the CrowdSec mode. + let crowdsec_mode_str = + env::var("CROWDSEC_MODE").unwrap_or_else(|_| panic!("$CROWDSEC_MODE is not set.")); + config.crowdsec_mode = match crowdsec_mode_str.as_str() { + "live" => CrowdSecMode::Live, + "none" => CrowdSecMode::None, + "stream" => CrowdSecMode::Stream, + _ => panic!("$CROWDSEC_MODE must be either 'stream', 'live' or 'none'."), + }; + + // Get the urls. + let use_https = match env::var("CROWDSEC_HTTPS") { + Ok(val) => match val.parse::() { + Ok(result) => result, + Err(_) => panic!("Failed to parse CROWDSEC_HTTPS value as boolean."), + }, + Err(_) => panic!("$CROWDSEC_HTTPS is not set."), + }; + let url = env::var("CROWDSEC_HOST").unwrap_or_else(|_| panic!("$CROWDSEC_HOST is not set.")); + if !url.starts_with("http://") && !url.starts_with("https://") { + config.crowdsec_live_url = (if use_https { "https://" } else { "http://" }).to_owned() + + &url + + CROWDSEC_LIVE_ROUTE; + config.crowdsec_stream_url = (if use_https { "https://" } else { "http://" }).to_owned() + + &url + + CROWDSEC_STREAM_ROUTE; + } + + // Get the API key. + config.crowdsec_api_key = + env::var("CROWDSEC_API_KEY").unwrap_or_else(|_| panic!("$CROWDSEC_API_KEY is not set.")); + + // Get the stream interval and cache ttl. + match config.crowdsec_mode { + CrowdSecMode::None => { + config.stream_interval = 0; + config.crowdsec_cache_ttl = 0; + } + CrowdSecMode::Live => { + config.stream_interval = 0; + config.crowdsec_cache_ttl = match env::var("LIVE_CACHE_EXPIRATION") { + Ok(val) => match val.parse::() { + Ok(result) => 1000 * result, + Err(_) => panic!("Failed to parse LIVE_CACHE_EXPIRATION value as integer."), + }, + Err(_) => panic!("$LIVE_CACHE_EXPIRATION is not set."), + }; + } + CrowdSecMode::Stream => { + config.stream_interval = match env::var("STREAM_UPDATE_INTERVAL") { + Ok(val) => match val.parse::() { + Ok(result) => result, + Err(_) => panic!("Failed to parse STREAM_UPDATE_INTERVAL value as integer."), + }, + Err(_) => panic!("$STREAM_UPDATE_INTERVAL is not set."), + }; + config.crowdsec_cache_ttl = 0; + } + } + + config +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..3a4fe9a --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,4 @@ +pub const APPLICATION_JSON: &str = "application/json"; +pub const TEXT_PLAIN: &str = "text/plain"; +pub const CROWDSEC_LIVE_ROUTE: &str = "/v1/decisions"; +pub const CROWDSEC_STREAM_ROUTE: &str = "/v1/decisions/stream"; diff --git a/src/crowdsec.rs b/src/crowdsec.rs new file mode 100644 index 0000000..dd594d8 --- /dev/null +++ b/src/crowdsec.rs @@ -0,0 +1,246 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use log::{error, info, warn}; + +use actix_web::rt::time; + +use ip_network_table_deps_treebitmap::IpLookupTable; + +use serde::{Deserialize, Serialize}; + +use crate::config::Config; +use crate::errors::{CrowdSecApiError}; +use crate::types::{CacheAttributes, HealthStatus}; +use crate::utils::get_ip_and_subnet; + +#[cfg(test)] +mod tests; + +/// The CrowdSec decision. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Decision { + /// The unique ID of the decision. + pub id: u32, + /// The origin of the decision (CAPI, LAPI, etc.). + pub origin: String, + /// The type of the decision (ban, etc.). + #[serde(rename = "type")] + pub _type: String, + /// The scope of the value (Ip, Range, etc.). + pub scope: String, + /// The IP Address or range to ban. + pub value: String, + /// The duration of the decision. + pub duration: String, + /// The reason for the decision. + pub scenario: String, +} + +/// The CrowdSec decision stream response. +#[derive(Debug, Deserialize, Serialize)] +pub struct Stream { + pub deleted: Option>, + pub new: Option>, +} + +fn set_health_status(health_status: Arc>, healthy: bool) { + if let Ok(mut health_status) = health_status.lock() { + health_status.stream_status = healthy; + } +} + +/// Call the CrowdSec API. +/// # Arguments +/// * `url` - The URL of the CrowdSec API. +/// * `api_key` - The API key to use to authenticate to the CrowdSec API. +/// # Returns +/// * The response of the CrowdSec API. +async fn get_request( + url: reqwest::Url, + api_key: &str, +) -> Result { + let client = reqwest::Client::new(); + client + .get(url) + .header("X-Api-Key", api_key) + .timeout(Duration::from_secs(10)) + .send() + .await +} + +/// Call the CrowdSec decisions API to know if the ip is banned or not. +/// # Arguments +/// * `url` - The URL of the CrowdSec decisions API. +/// * `api_key` - The API key to use to authenticate to the CrowdSec decisions API. +/// * `ip` - The IP address to check. +/// # Returns +/// * `Some(Decision)` if the IP address is banned, `None` otherwise. +/// * `CrowdSecApiError` if the request failed. +pub async fn get_decision( + url: &str, + api_key: &str, + ip: &str, +) -> Result, CrowdSecApiError> { + let params = [("ip", ip), ("type", "ban")]; + let url = reqwest::Url::parse_with_params(url, ¶ms) + .map_err(|err| CrowdSecApiError::UrlParsingFailed { error: err })?; + let res = get_request(url, api_key) + .await + .map_err(|err| CrowdSecApiError::RequestFailed { error: err })?; + if !res.status().is_success() { + return Err(CrowdSecApiError::ResponseBad { + status_code: res.status(), + }); + } + if let Ok(text) = res.text().await { + if text == "null" { + return Ok(None); + } + if let Ok(decision) = serde_json::from_str::>(&text) { + if !decision.is_empty() { + return Ok(Some(decision[0].clone())); + } + } + } + Err(CrowdSecApiError::ResponseParsingFailed { + error: String::from("Could not parse decision in response."), + }) +} + +/// Call the CrowdSec decisions stream API to get the new and deleted decisions. +/// # Arguments +/// * `url` - The URL of the CrowdSec decisions stream API. +/// * `api_key` - The API key to use to authenticate to the CrowdSec decisions stream API. +/// * `startup` - If `true`, the API will return all the decisions, otherwise it will only return the new decisions. +/// # Returns +/// * The new and deleted decisions. +/// * `CrowdSecApiError` if the request failed. +async fn get_decisions_stream( + url: &str, + api_key: &str, + startup: bool, +) -> Result { + let params = [ + ("startup", if startup { "true" } else { "false" }), + ("scope", "Ip,Range"), + ]; + let url = reqwest::Url::parse_with_params(url, ¶ms) + .map_err(|err| CrowdSecApiError::UrlParsingFailed { error: err })?; + let res = get_request(url, api_key) + .await + .map_err(|err| CrowdSecApiError::RequestFailed { error: err })?; + if !res.status().is_success() { + return Err(CrowdSecApiError::ResponseBad { + status_code: res.status(), + }); + } + if let Ok(stream) = res.json::().await { + return Ok(stream); + } + Err(CrowdSecApiError::ResponseParsingFailed { + error: String::from("Could not parse new/deleted decisions in response."), + }) +} + +/// The main function. +/// Updates the IP lookup tables with the new and deleted decisions at a regular interval. +/// # Arguments +/// * `config` - The configuration. +/// * `health_status` - The health status. +/// * `ipv4_table` - The IPv4 lookup table. +/// * `ipv6_table` - The IPv6 lookup table. +pub async fn stream( + config: Config, + health_status: Arc>, + ipv4_table: Arc>>, + ipv6_table: Arc>>, +) { + let mut startup: bool = true; + let mut interval = time::interval(Duration::from_secs(config.stream_interval)); + loop { + interval.tick().await; + + // let mut ipv4_table_tmp = ipv4_table.lock().unwrap(); + // warn!("Initial insert"); + // ipv4_table_tmp.insert(Ipv4Addr::new(1, 2, 3, 4), 32, true); + // continue; + + match get_decisions_stream( + &config.crowdsec_stream_url, + &config.crowdsec_api_key, + startup, + ) + .await + { + Ok(stream) => { + set_health_status(health_status.clone(), true); + if stream.new.is_none() && stream.deleted.is_none() { + continue; + } + info!("Decisions stream: {:?}", stream); + + if let Some(ref new) = stream.new { + for decision in new { + let range = get_ip_and_subnet(&decision.value); + match range { + Some(range) => match range.ipv4 { + Some(ipv4) => { + if let Ok(mut table) = ipv4_table.lock() { + table.insert( + ipv4, + range.subnet.unwrap_or(32), + CacheAttributes::new(false, 0), + ); + } + } + None => match range.ipv6 { + Some(ipv6) => { + if let Ok(mut table) = ipv6_table.lock() { + table.insert( + ipv6, + range.subnet.unwrap_or(128), + CacheAttributes::new(false, 0), + ); + } + } + None => warn!("Invalid IP (in new): {:?}", decision.value), + }, + }, + None => warn!("Invalid IP (in new): {:?}", decision.value), + } + } + } + if let Some(ref deleted) = stream.deleted { + for decision in deleted { + let range = get_ip_and_subnet(&decision.value); + match range { + Some(range) => match range.ipv4 { + Some(ipv4) => { + if let Ok(mut table) = ipv4_table.lock() { + table.remove(ipv4, range.subnet.unwrap_or(32)); + } + } + None => match range.ipv6 { + Some(ipv6) => { + if let Ok(mut table) = ipv6_table.lock() { + table.remove(ipv6, range.subnet.unwrap_or(128)); + } + } + None => warn!("Invalid IP (in deleted): {:?}", decision.value), + }, + }, + None => warn!("Invalid IP (in deleted): {:?}", decision.value), + } + } + } + startup = false; + } + Err(err) => { + error!("Could not call API. Error: {}", err); + set_health_status(health_status.clone(), false); + } + } + } +} diff --git a/src/crowdsec/tests.rs b/src/crowdsec/tests.rs new file mode 100644 index 0000000..5701299 --- /dev/null +++ b/src/crowdsec/tests.rs @@ -0,0 +1,174 @@ +use super::*; + +#[tokio::test] +async fn test_get_decision_banned_ip() { + let api_key = "my_api_key"; + let ip = "1.2.3.4"; + + // Simulate a banned IP address. + let mock_response = serde_json::json!([ + { + "duration": "33h6m18.03174611s", + "id": 1, + "origin": "CAPI", + "scenario": "abc", + "scope": "Ip", + "type": "ban", + "value": "1.2.3.4" + } + ]); + let mut server = mockito::Server::new(); + let mock_server = server + .mock("GET", "/v1/decisions") + .match_header("X-Api-Key", api_key) + .match_query(format!("ip={}&type=ban", ip).as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(mock_response.to_string()) + .create_async() + .await; + + // Get the decision. + let url = server.url() + "/v1/decisions"; + let result = get_decision(&url, api_key, ip).await.unwrap(); + + // Verify that the function returns the expected result. + assert!(result.is_some()); + assert_eq!(result.unwrap().value, ip); + + // Clean up the mock server. + mock_server.assert(); +} + +#[tokio::test] +async fn test_get_decision_unbanned_ip() { + let api_key = "my_api_key"; + let ip = "1.2.3.4"; + + // Simulate an unbanned IP address. + let mock_response = "null"; + let mut server = mockito::Server::new(); + let mock_server = server + .mock("GET", "/v1/decisions") + .match_header("X-Api-Key", api_key) + .match_query(format!("ip={}&type=ban", ip).as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(mock_response) + .create(); + + let url = server.url() + "/v1/decisions"; + let result = get_decision(&url, api_key, ip).await.unwrap(); + + // Verify that the function returns the expected result. + assert!(result.is_none()); + + // Clean up the mock server. + mock_server.assert(); +} + +#[tokio::test] +async fn test_get_decision_bad_response() { + let api_key = "my_api_key"; + let ip = "1.2.3.4"; + + // Simulate a bad response from the API. + let mut server = mockito::Server::new(); + let mock_server = server + .mock("GET", "/v1/decisions") + .match_header("X-Api-Key", api_key) + .match_query(format!("ip={}&type=ban", ip).as_str()) + .with_status(400) + .create(); + + // Get the decision. + let url = server.url() + "/v1/decisions"; + let result = get_decision(&url, api_key, ip).await; + + // Verify that the function returns an error. + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + CrowdSecApiError::ResponseBad { .. } + )); + + // Clean up the mock server. + mock_server.assert(); +} + +#[tokio::test] +async fn test_get_decisions_stream_startup() { + let api_key = "my_api_key"; + let startup = true; + + // Simulate a stream of decisions. + let mock_response = serde_json::json!({ + "new": [ + { + "duration": "33h6m18.03174611s", + "id": 1, + "origin": "CAPI", + "scenario": "abc", + "scope": "Ip", + "type": "ban", + "value": "1.2.3.4" + }, + { + "duration": "33h6m18.03174611s", + "id": 1, + "origin": "CAPI", + "scenario": "abc", + "scope": "Ip", + "type": "ban", + "value": "1.2.3.5" + } + ], + "deleted": [ + { + "duration": "33h6m18.03174611s", + "id": 1, + "origin": "CAPI", + "scenario": "abc", + "scope": "Ip", + "type": "ban", + "value": "1.1.1.1" + }, + { + "duration": "33h6m18.03174611s", + "id": 1, + "origin": "CAPI", + "scenario": "abc", + "scope": "Ip", + "type": "ban", + "value": "1.1.1.2" + } + ] + }); + let mut server = mockito::Server::new(); + let mock_server = server + .mock("GET", "/v1/decisions/stream") + .match_header("X-Api-Key", api_key) + .match_query(format!("startup=true&scope=Ip%2CRange").as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(mock_response.to_string()) + .create(); + + // Get the decision stream. + let url = server.url() + "/v1/decisions/stream"; + let result = get_decisions_stream(&url, api_key, startup).await; + + assert!(result.is_ok()); + let stream = result.unwrap(); + let new = stream.new.unwrap(); + assert_eq!(2, new.len()); + assert_eq!("1.2.3.4", new[0].value); + assert_eq!("1.2.3.5", new[1].value); + let deleted = stream.deleted.unwrap(); + assert_eq!(2, deleted.len()); + assert_eq!("1.1.1.1", deleted[0].value); + assert_eq!("1.1.1.2", deleted[1].value); + + // Clean up the mock server. + mock_server.assert(); +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..b79d149 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,11 @@ +use custom_error::custom_error; + +use url::ParseError; + +custom_error! { + pub CrowdSecApiError + RequestFailed{error: reqwest::Error} = "CrowdSec API call failed. Error: {error}", + ResponseBad{status_code: reqwest::StatusCode} = "CrowdSec API call not successful. Status code: {status_code}", + ResponseParsingFailed{error: String} = "CrowdSec response parsing failed. Error: {error}", + UrlParsingFailed{error: ParseError} = "CrowdSec url parsing failed. Error: {error}", +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2246b9c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,83 @@ +#[macro_use] +extern crate actix_web; + +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::sync::{Arc, Mutex}; +use std::{env, io}; + +use log::info; + +use actix_web::{middleware, web, App, HttpServer}; + +use crate::types::HealthStatus; +use ip_network_table_deps_treebitmap::IpLookupTable; + +mod bouncer; +mod config; +mod constants; +mod crowdsec; +mod errors; +mod types; +mod utils; + +use crate::types::CacheAttributes; + +#[actix_web::main] +async fn main() -> io::Result<()> { + env::set_var("RUST_LOG", "info,actix_web=info,actix_server=info"); + env_logger::init(); + info!("Starting Bouncer."); + + info!("Reading configuration."); + let config = config::read_config(); + let config_clone = config.clone(); + + let health_status = Arc::new(Mutex::new(HealthStatus::new())); + let health_status_clone = health_status.clone(); + let ipv4_table = Arc::new(Mutex::new(IpLookupTable::::new())); + let ipv6_table = Arc::new(Mutex::new(IpLookupTable::::new())); + let ipv4_table_clone = ipv4_table.clone(); + let ipv6_table_clone = ipv6_table.clone(); + + match config.crowdsec_mode { + config::CrowdSecMode::Stream => { + info!("Starting CrowdSec stream update."); + // Update the IP tables from CrowdSec stream. + actix_rt::spawn(async move { + crowdsec::stream( + config_clone, + health_status_clone, + ipv4_table_clone, + ipv6_table_clone, + ) + .await; + }); + } + config::CrowdSecMode::None => { + info!("No mode configured. Ask CrowdSec API every time."); + } + config::CrowdSecMode::Live => { + info!("Live mode configured."); + } + } + + // API. + info!("Starting HTTP server (API)."); + HttpServer::new(move || { + App::new() + // Enable the logger - always register actix-web Logger middleware last. + .wrap(middleware::Logger::default()) + // App data. + .app_data(web::Data::new(config.clone())) + .app_data(web::Data::new(health_status.clone())) + .app_data(web::Data::new(ipv4_table.clone())) + .app_data(web::Data::new(ipv6_table.clone())) + // Register HTTP requests handlers. + .service(bouncer::authenticate) + .service(bouncer::block_list) + .service(bouncer::health) + }) + .bind("0.0.0.0:9090")? + .run() + .await +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..083fe69 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,34 @@ +/// Attributes for the IP entry in the lookup table. +pub struct CacheAttributes { + /// Whether the IP is allowed or not. + pub allowed: bool, + /// The expiration time of the IP. + pub expiration_time: i64, +} + +impl CacheAttributes { + pub fn new(allowed: bool, expiration_time: i64) -> CacheAttributes { + CacheAttributes { + allowed, + expiration_time, + } + } +} + +/// The health status of the application. +pub struct HealthStatus { + pub live_status: bool, + pub stream_status: bool, +} + +impl HealthStatus { + pub fn new() -> HealthStatus { + HealthStatus { + live_status: true, + stream_status: true, + } + } + pub fn healthy(&self) -> bool { + self.live_status && self.stream_status + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..70b9ceb --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,66 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +#[cfg(test)] +mod tests; + +/// The IP address and optional subnet. +pub struct Address { + /// The IPv4 address. + pub ipv4: Option, + /// The IPv6 address. + pub ipv6: Option, + /// The subnet. + pub subnet: Option, +} + +/// Get the IP address (IPv4 or IPv6) and optional subnet from a string. +/// # Arguments +/// * `ip` - The IP address and optional subnet. +/// # Returns +/// * The IP address (IPv4 or IPv6) and optional subnet or `None` if the IP address is invalid. +pub fn get_ip_and_subnet(ip: &str) -> Option
{ + fn try_to_convert_to_ipv4(ip: &str, subnet: Option) -> Option
{ + let addr = ip.parse::().ok()?; + if let Some(subnet) = subnet { + if subnet > 32 { + return None; + } + } + Some(Address { + ipv4: Some(addr), + ipv6: None, + subnet, + }) + } + + fn try_to_convert_to_ipv6(ip: &str, subnet: Option) -> Option
{ + let addr = ip.parse::().ok()?; + if let Some(subnet) = subnet { + if subnet > 128 { + return None; + } + } + Some(Address { + ipv4: None, + ipv6: Some(addr), + subnet, + }) + } + + if !ip.contains('/') { + if ip.contains(':') { + return try_to_convert_to_ipv6(ip, None); + } + return try_to_convert_to_ipv4(ip, None); + } + let parts: Vec<&str> = ip.split('/').collect(); + if parts.len() != 2 { + return None; + } + let ip_address = parts[0]; + let parsed_subnet = parts[1].parse::().ok()?; + if ip_address.contains(':') { + return try_to_convert_to_ipv6(ip_address, Some(parsed_subnet)); + } + try_to_convert_to_ipv4(ip_address, Some(parsed_subnet)) +} diff --git a/src/utils/tests.rs b/src/utils/tests.rs new file mode 100644 index 0000000..cf6b0ae --- /dev/null +++ b/src/utils/tests.rs @@ -0,0 +1,50 @@ +use super::*; + +#[test] +async fn test_get_ip_and_subnet() { + let ip = "1.1.1.1"; + let range = get_ip_and_subnet(ip); + assert!(range.is_some()); + let range = range.unwrap(); + assert!(range.ipv4.is_some()); + assert!(range.ipv6.is_none()); + assert!(range.subnet.is_none()); + + let ip = "1.1.1.1/24"; + let range = get_ip_and_subnet(ip); + assert!(range.is_some()); + let range = range.unwrap(); + assert!(range.ipv4.is_some()); + assert!(range.ipv6.is_none()); + assert!(range.subnet.is_some()); + assert_eq!(range.subnet.unwrap(), 24); + + let ip = "1.1.1"; // invalid IP + let range = get_ip_and_subnet(ip); + assert!(range.is_none()); + + let ip = "1.1.1.1/42"; // invalid subnet + let range = get_ip_and_subnet(ip); + assert!(range.is_none()); + + let ip = "2001:db8::8a2e:370:7334"; // IPv6 + let range = get_ip_and_subnet(ip); + assert!(range.is_some()); + let range = range.unwrap(); + assert!(range.ipv4.is_none()); + assert!(range.ipv6.is_some()); + assert!(range.subnet.is_none()); + + let ip = "2001:db8::8a2e:370:7334/24"; // IPv6 + let range = get_ip_and_subnet(ip); + assert!(range.is_some()); + let range = range.unwrap(); + assert!(range.ipv4.is_none()); + assert!(range.ipv6.is_some()); + assert!(range.subnet.is_some()); + assert_eq!(range.subnet.unwrap(), 24); + + let ip = "2001:db8::8a2e:370:7334/129"; // invalid subnet + let range = get_ip_and_subnet(ip); + assert!(range.is_none()); +}