diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..d3ce8ab --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,31 @@ +name: Tag latest version + +on: + workflow_dispatch: + push: + branches: [ main ] + +concurrency: tag + +jobs: + tag-release: + runs-on: ubuntu-latest + steps: + - uses: cachix/install-nix-action@master + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v4 + + - name: Read version + run: | + echo -n "_version=v" >> "$GITHUB_ENV" + nix run nixpkgs#fq -- -r ".package.version" Cargo.toml >> "$GITHUB_ENV" + cat "$GITHUB_ENV" + + - name: Tag + run: | + set -x + git tag $version + git push --tags || : diff --git a/Cargo.lock b/Cargo.lock index 712bec0..2eaaf07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.12.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" dependencies = [ "actix-codec", "actix-rt", @@ -49,7 +49,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.2", + "rand", "sha1", "smallvec", "tokio", @@ -65,14 +65,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "actix-router" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" dependencies = [ "bytestring", "cfg-if", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" dependencies = [ "futures-core", "tokio", @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.6.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" dependencies = [ "actix-rt", "actix-service", @@ -105,7 +105,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.10", + "socket2", "tokio", "tracing", ] @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.13.0" +version = "4.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" dependencies = [ "actix-codec", "actix-http", @@ -167,7 +167,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.2", + "socket2", "time", "tracing", "url", @@ -182,20 +182,29 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", ] [[package]] name = "adler2" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -215,6 +224,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -226,9 +241,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -241,45 +256,39 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.5" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.11" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", + "once_cell", + "windows-sys 0.59.0", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - [[package]] name = "ascii" version = "1.1.0" @@ -288,9 +297,24 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] [[package]] name = "base64" @@ -300,9 +324,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -315,9 +339,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -326,9 +350,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -336,9 +360,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "serde", @@ -346,32 +370,31 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytes" -version = "1.11.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.2.56" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ - "find-msvc-tools", "jobserver", "libc", "shlex", @@ -379,31 +402,19 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", -] +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", - "wasm-bindgen", "windows-link", ] @@ -415,9 +426,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "clap" -version = "4.5.60" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -425,11 +436,10 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ - "anstream", "anstyle", "clap_lex", "strsim", @@ -437,36 +447,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "clap_lex" -version = "1.0.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "cookie" @@ -494,29 +495,20 @@ dependencies = [ "libc", ] -[[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" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -524,33 +516,31 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "derive_more" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version", - "syn 2.0.117", + "syn 2.0.101", "unicode-xid", ] @@ -572,7 +562,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] @@ -592,9 +582,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -608,9 +598,9 @@ checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -627,7 +617,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "eris" -version = "0.2.0" +version = "1.0.0" dependencies = [ "actix-web", "chrono", @@ -639,35 +629,37 @@ dependencies = [ "log", "prometheus 0.14.0", "prometheus_exporter", - "rand 0.10.0", + "rand", + "regex", "rlua", "serde", "serde_json", - "thiserror 2.0.18", + "tempfile", "tokio", + "toml", ] [[package]] name = "errno" -version = "0.3.14" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -687,18 +679,18 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -711,9 +703,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -721,15 +713,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -738,38 +730,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -779,6 +771,7 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] @@ -794,35 +787,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] -name = "getrandom" -version = "0.4.1" +name = "gimli" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core 0.10.0", - "wasip2", - "wasip3", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -839,18 +824,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -883,9 +859,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "iana-time-zone" -version = "0.1.65" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -907,22 +883,21 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -932,71 +907,103 @@ dependencies = [ ] [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "icu_locid_transform" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ + "displaydoc", "icu_collections", - "icu_locale_core", + "icu_locid_transform", "icu_properties_data", "icu_provider", - "zerotrie", + "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", - "icu_locale_core", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", - "zerotrie", "zerovec", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "icu_provider_macros" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] name = "idna" -version = "1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -1005,9 +1012,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", @@ -1021,14 +1028,12 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -1039,9 +1044,9 @@ checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" [[package]] name = "is_terminal_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -1054,49 +1059,49 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.22" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde_core", + "serde", ] [[package]] name = "jiff-static" -version = "0.2.22" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.4", + "getrandom", "libc", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -1114,29 +1119,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.182" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" -version = "0.12.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "local-channel" @@ -1157,18 +1156,19 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.14" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.29" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lua-src" @@ -1191,9 +1191,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1203,24 +1203,23 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", - "simd-adler32", ] [[package]] name = "mio" -version = "1.1.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.61.2", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -1239,9 +1238,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.6.8" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594" dependencies = [ "cc", "cfg-if", @@ -1262,14 +1261,14 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" @@ -1280,23 +1279,26 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "parking_lot" -version = "0.12.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1304,28 +1306,34 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.12" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-targets", ] [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" @@ -1335,28 +1343,19 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1372,16 +1371,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1408,9 +1397,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1441,7 +1430,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.18", + "thiserror 2.0.12", ] [[package]] @@ -1480,38 +1469,27 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" -dependencies = [ - "chacha20", - "getrandom 0.4.1", - "rand_core 0.10.0", + "rand_core", ] [[package]] @@ -1521,38 +1499,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.4", + "getrandom", ] -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1562,9 +1534,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1573,15 +1545,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.9" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rlua" @@ -1592,45 +1564,42 @@ dependencies = [ "mlua", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" -version = "1.1.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scopeguard" @@ -1638,53 +1607,45 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - [[package]] name = "serde" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ "serde", - "serde_core", - "zmij", ] [[package]] @@ -1706,7 +1667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1718,57 +1679,43 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.8" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ - "errno", "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - [[package]] name = "slab" -version = "0.4.12" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.10" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - [[package]] name = "stable_deref_trait" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strsim" @@ -1788,9 +1735,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -1805,7 +1752,20 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -1819,11 +1779,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.12", ] [[package]] @@ -1834,46 +1794,46 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde_core", + "serde", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -1894,9 +1854,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -1904,37 +1864,38 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ + "backtrace", "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -1944,10 +1905,51 @@ dependencies = [ ] [[package]] -name = "tracing" -version = "0.1.44" +name = "toml" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -1957,41 +1959,35 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" @@ -2001,16 +1997,21 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "url" -version = "2.5.8" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2031,46 +2032,50 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2078,60 +2083,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "which" version = "7.0.3" @@ -2146,9 +2117,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", @@ -2159,46 +2130,46 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] name = "windows-link" -version = "0.2.1" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" -version = "0.4.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.5.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ "windows-link", ] @@ -2209,25 +2180,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", + "windows-targets", ] [[package]] @@ -2236,31 +2198,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2269,84 +2214,42 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2354,10 +2257,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" +dependencies = [ + "memchr", +] [[package]] name = "winsafe" @@ -2366,105 +2272,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", ] [[package]] -name = "wit-parser" -version = "0.244.0" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" -version = "0.6.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2472,34 +2306,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] [[package]] @@ -2519,26 +2353,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", "synstructure", ] -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - [[package]] name = "zerovec" -version = "0.11.5" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -2547,21 +2370,15 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.101", ] -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - [[package]] name = "zstd" version = "0.13.3" @@ -2582,9 +2399,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 133e86b..42cc24b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,24 @@ [package] name = "eris" -description = "Sophisticated HTTP tarpit and honeypot stream" -authors = ["NotAShelf B[Eris (port 80)] - B --> C[Nginx (local port)] +``` +Internet → [Eris (port 80)] → [Nginx (local port)] ``` You will want to configure Eris to listen on port 80 (or 443 for SSL) and @@ -134,11 +132,8 @@ eris --listen-addr 0.0.0.0:443 --backend-addr 127.0.0.1:8080 --ssl-cert /path/to ### Option 2: Use a separate SSL terminator -```mermaid -graph LR - A[Internet] --> B[SSL Terminator (port 443)] - B --> C[Eris (local port)] - C --> D[Nginx (local port)] +``` +Internet → [SSL Terminator (port 443)] → [Eris (local port)] → [Nginx (local port)] ``` You can use Nginx, HAProxy, or Caddy as the SSL terminator, forwarding decrypted diff --git a/flake.lock b/flake.lock index e7e184a..8372b45 100644 --- a/flake.lock +++ b/flake.lock @@ -1,27 +1,12 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1772080396, - "narHash": "sha256-84W9UNtSk9DNMh43WBkOjpkbfODlmg+RDi854PnNgLE=", - "owner": "ipetkov", - "repo": "crane", - "rev": "8525580bc0316c39dbfa18bd09a1331e98c9e463", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1772198003, - "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", + "lastModified": 1745930157, + "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", + "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", "type": "github" }, "original": { @@ -33,7 +18,6 @@ }, "root": { "inputs": { - "crane": "crane", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index b5b8db7..9143653 100644 --- a/flake.nix +++ b/flake.nix @@ -1,15 +1,11 @@ { - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; - crane.url = "github:ipetkov/crane"; - }; + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; outputs = { self, nixpkgs, - crane, }: let - systems = ["x86_64-linux" "aarch64-linux"]; + systems = ["x86_64-linux"]; forEachSystem = nixpkgs.lib.genAttrs systems; pkgsForEach = nixpkgs.legacyPackages; in { @@ -18,10 +14,8 @@ default = self.nixosModules.eris; }; - packages = forEachSystem (system: let - craneLib = crane.mkLib pkgsForEach.${system}; - in { - eris = pkgsForEach.${system}.callPackage ./nix/package.nix {inherit craneLib;}; + packages = forEachSystem (system: { + eris = pkgsForEach.${system}.callPackage ./nix/package.nix {}; default = self.packages.${system}.eris; }); diff --git a/nix/package.nix b/nix/package.nix index adb4195..aa6e975 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,50 +1,40 @@ { lib, - craneLib, - pkg-config, - openssl, + rustPlatform, }: let - pname = "eris"; - inherit ((lib.importTOML ../Cargo.toml).package) version; - src = let - fs = lib.fileset; - s = ../.; - in - fs.toSource { - root = s; - fileset = fs.unions [ - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) - (s + /Cargo.lock) - (s + /Cargo.toml) - ]; - }; + fs = lib.fileset; - cargoArtifacts = craneLib.buildDepsOnly { - name = "eris"; - inherit src; - - strictDeps = true; - nativeBuildInputs = [pkg-config]; - buildInputs = [openssl]; - }; + lockfile = ../Cargo.lock; + cargoToml = ../Cargo.toml; in - craneLib.buildPackage { - inherit pname src version cargoArtifacts; + rustPlatform.buildRustPackage { + pname = "eris"; + version = "0.0.1"; - strictDeps = true; + src = let + s = ../.; + in + fs.toSource { + root = s; + fileset = fs.unions [ + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) + (s + /contrib) + (s + /resources) + lockfile + cargoToml + ]; + }; - # FIXME: better provide those via wrappers... - # postFixup = '' - # mkdir -p "$out"/share/contrib/{corpus,lua} - # - # install -Dm755 ${../contrib/corpus}/*.txt $out/share/contrib/corpus - # install -Dm755 ${../contrib/lua}/*.lua $out/share/contrib/lua - # ''; + postInstall = '' + mkdir -p $out/share/contrib + cp -rv $src/contrib/corpus $out/share/contrib + cp -rv $src/contrib/lua $out/share/contrib + ''; + + cargoLock.lockFile = lockfile; meta = { description = "Sophisticated HTTP tarpit and honeypot stream"; - homepage = "https://git.frzn.dev/NotAShelf/eris"; - maintainers = [lib.maintainers.NotAShelf]; mainProgram = "eris"; }; } diff --git a/nix/shell.nix b/nix/shell.nix index 9df0432..5df9c82 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,26 +1,28 @@ { mkShell, - rustc, - cargo, + rust-analyzer, rustfmt, clippy, - taplo, - rust-analyzer-unwrapped, - rustPlatform, + cargo, + gcc, + openssl, + pkg-config, + rustc, }: mkShell { - name = "rust"; - + name = "eris"; packages = [ - rustc - cargo - - (rustfmt.override {asNightly = true;}) + rust-analyzer + rustfmt clippy cargo - taplo - rust-analyzer-unwrapped - ]; + gcc + clippy + rustfmt + rustc - RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; + # For TLS and friends + openssl + pkg-config + ]; } diff --git a/resources/default_script.lua b/resources/default_script.lua new file mode 100644 index 0000000..f2f51fd --- /dev/null +++ b/resources/default_script.lua @@ -0,0 +1,210 @@ +--[[ +Eris Default Script + +This script demonstrates how to use the Eris Lua API to customize +the tarpit's behavior, and will be loaded by default if no other +scripts are loaded. + +Available events: +- connection: When a new connection is established +- request: When a request is received +- response_gen: When generating a response +- response_chunk: Before sending each response chunk +- disconnection: When a connection is closed +- block_ip: When an IP is being considered for blocking +- startup: When the application starts +- shutdown: When the application is shutting down +- periodic: Called periodically + +API Functions: +- eris.debug(message): Log a debug message +- eris.info(message): Log an info message +- eris.warn(message): Log a warning message +- eris.error(message): Log an error message +- eris.set_state(key, value): Store persistent state +- eris.get_state(key): Retrieve persistent state +- eris.inc_counter(key, [amount]): Increment a counter +- eris.get_counter(key): Get a counter value +- eris.gen_token([prefix]): Generate a unique token +- eris.timestamp(): Get current Unix timestamp +--]] + +-- Called when the application starts +eris.on("startup", function(ctx) + eris.info("Initializing default script") + + -- Initialize counters + eris.inc_counter("total_connections", 0) + eris.inc_counter("total_responses", 0) + eris.inc_counter("blocked_ips", 0) + + -- Initialize banned keywords + eris.set_state("banned_keywords", "eval,exec,system,shell," + .. "\n" + .. "\n" + elseif ctx.path:find("phpunit") or ctx.path:find("eval") then + -- For PHP exploit attempts + -- Turns out you can just google "PHP error log" and search random online forums where people + -- dump their service logs in full. + enhanced_content = enhanced_content + .. "\nPHP Notice: Undefined variable: _SESSION in /var/www/html/includes/core.php on line 58\n" + .. "Warning: file_get_contents(): Filename cannot be empty in /var/www/html/vendor/autoload.php on line 23\n" + .. "Token: " + .. token + .. "\n" + elseif ctx.path:find("api") then + -- For API requests + local fake_api_key = + string.format("ak_%x%x%x", math.random(1000, 9999), math.random(1000, 9999), math.random(1000, 9999)) + + enhanced_content = enhanced_content + .. "{\n" + .. ' "status": "warning",\n' + .. ' "message": "Test API environment detected",\n' + .. ' "debug_token": "' + .. token + .. '",\n' + .. ' "api_key": "' + .. fake_api_key + .. '"\n' + .. "}\n" + else + -- For other requests + enhanced_content = enhanced_content + .. "\n" + .. "\n" + .. "\n" + end + + -- Track which honeytokens were sent to which IP + local honeytokens = eris.get_state("honeytokens") or "{}" + local ht_table = {} + + -- This is a simplistic approach - in a real script, you'd want to use + -- a proper JSON library to handle this correctly + if honeytokens ~= "{}" then + -- Simple parsing of the stored data + for ip, tok in honeytokens:gmatch('"([^"]+)":"([^"]+)"') do + ht_table[ip] = tok + end + end + + ht_table[ctx.ip] = token + + -- Convert back to a simple JSON-like string + local new_tokens = "{" + for ip, tok in pairs(ht_table) do + if new_tokens ~= "{" then + new_tokens = new_tokens .. "," + end + new_tokens = new_tokens .. '"' .. ip .. '":"' .. tok .. '"' + end + new_tokens = new_tokens .. "}" + + eris.set_state("honeytokens", new_tokens) + + return enhanced_content +end) + +-- Called before sending each chunk of a response +eris.on("response_chunk", function(ctx) + -- This can be used to alter individual chunks for more deceptive behavior + -- For example, to simulate a slow, unreliable server + + -- 5% chance of "corrupting" a chunk to confuse scanners + if math.random(1, 100) <= 5 then + local chunk = ctx.content + if #chunk > 10 then + local pos = math.random(1, #chunk - 5) + chunk = chunk:sub(1, pos) .. string.char(math.random(32, 126)) .. chunk:sub(pos + 2) + end + return chunk + end + + return ctx.content +end) + +-- Called when deciding whether to block an IP +eris.on("block_ip", function(ctx) + -- You can override the default blocking logic + + -- Check for potential attackers using specific patterns + local banned_keywords = eris.get_state("banned_keywords") or "" + local user_agent = ctx.user_agent or "" + + -- Check if user agent contains highly suspicious patterns + for keyword in banned_keywords:gmatch("[^,]+") do + if user_agent:lower():find(keyword:lower()) then + eris.info("Blocking IP " .. ctx.ip .. " due to suspicious user agent: " .. keyword) + eris.inc_counter("blocked_ips") + return true -- Force block + end + end + + -- For demonstration, we'll be more lenient with 10.x IPs + if ctx.ip:match("^10%.") then + -- Only block if they've hit us many times + return ctx.hit_count >= 5 + end + + -- Default to the system's threshold-based decision + return nil +end) + +-- The enhance_response is now legacy, and I never liked it anyway. Though let's add it here +-- for the sake of backwards compatibility. +function enhance_response(text, response_type, path, token) + local enhanced = text + + -- Add token as a comment + if response_type == "php_exploit" then + enhanced = enhanced .. "\n/* Token: " .. token .. " */\n" + elseif response_type == "wordpress" then + enhanced = enhanced .. "\n\n" + elseif response_type == "api" then + enhanced = enhanced:gsub('"status": "[^"]+"', '"status": "warning"') + enhanced = enhanced:gsub('"message": "[^"]+"', '"message": "API token: ' .. token .. '"') + else + enhanced = enhanced .. "\n\n" + end + + return enhanced +end diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..74a1571 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,613 @@ +use clap::Parser; +use ipnetwork::IpNetwork; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::env; +use std::fs; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; + +// Command-line arguments using clap +#[derive(Parser, Debug, Clone)] +#[clap( + author, + version, + about = "Markov chain based HTTP tarpit/honeypot that delays and tracks potential attackers" +)] +pub struct Args { + #[clap( + long, + default_value = "0.0.0.0:8888", + help = "Address and port to listen for incoming HTTP requests (format: ip:port)" + )] + pub listen_addr: String, + + #[clap( + long, + default_value = "0.0.0.0:9100", + help = "Address and port to expose Prometheus metrics and status endpoint (format: ip:port)" + )] + pub metrics_addr: String, + + #[clap(long, help = "Disable Prometheus metrics server completely")] + pub disable_metrics: bool, + + #[clap( + long, + default_value = "127.0.0.1:80", + help = "Backend server address to proxy legitimate requests to (format: ip:port)" + )] + pub backend_addr: String, + + #[clap( + long, + default_value = "1000", + help = "Minimum delay in milliseconds between chunks sent to attacker" + )] + pub min_delay: u64, + + #[clap( + long, + default_value = "15000", + help = "Maximum delay in milliseconds between chunks sent to attacker" + )] + pub max_delay: u64, + + #[clap( + long, + default_value = "600", + help = "Maximum time in seconds to keep an attacker in the tarpit before disconnecting" + )] + pub max_tarpit_time: u64, + + #[clap( + long, + default_value = "3", + help = "Number of hits to honeypot patterns before permanently blocking an IP" + )] + pub block_threshold: u32, + + #[clap( + long, + help = "Base directory for all application data (overrides XDG directory structure)" + )] + pub base_dir: Option, + + #[clap( + long, + help = "Path to configuration file (JSON or TOML, overrides command line options)" + )] + pub config_file: Option, + + #[clap( + long, + default_value = "info", + help = "Log level: trace, debug, info, warn, error" + )] + pub log_level: String, + + #[clap( + long, + default_value = "pretty", + help = "Log format: plain, pretty, json, pretty-json" + )] + pub log_format: String, + + #[clap(long, help = "Enable rate limiting for connections from the same IP")] + pub rate_limit_enabled: bool, + + #[clap(long, default_value = "60", help = "Rate limit window in seconds")] + pub rate_limit_window: u64, + + #[clap( + long, + default_value = "30", + help = "Maximum number of connections allowed per IP in the rate limit window" + )] + pub rate_limit_max: usize, + + #[clap( + long, + default_value = "100", + help = "Connection attempts threshold before considering for IP blocking" + )] + pub rate_limit_block_threshold: usize, + + #[clap( + long, + help = "Send a 429 response for rate limited connections instead of dropping connection" + )] + pub rate_limit_slow_response: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum LogFormat { + Plain, + #[default] + Pretty, + Json, + PrettyJson, +} + +// Trap pattern structure. It can be either a plain string +// regex to catch more advanced patterns necessitated by +// more sophisticated crawlers. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum TrapPattern { + Plain(String), + Regex { pattern: String, regex: bool }, +} + +impl TrapPattern { + pub fn as_plain(value: &str) -> Self { + Self::Plain(value.to_string()) + } + + pub fn as_regex(value: &str) -> Self { + Self::Regex { + pattern: value.to_string(), + regex: true, + } + } + + pub fn matches(&self, path: &str) -> bool { + match self { + Self::Plain(pattern) => path.contains(pattern), + Self::Regex { + pattern, + regex: true, + } => { + if let Ok(re) = Regex::new(pattern) { + re.is_match(path) + } else { + false + } + } + _ => false, + } + } +} + +// Configuration structure +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Config { + pub listen_addr: String, + pub metrics_addr: String, + pub disable_metrics: bool, + pub backend_addr: String, + pub min_delay: u64, + pub max_delay: u64, + pub max_tarpit_time: u64, + pub block_threshold: u32, + pub trap_patterns: Vec, + pub whitelist_networks: Vec, + pub markov_corpora_dir: String, + pub lua_scripts_dir: String, + pub data_dir: String, + pub config_dir: String, + pub cache_dir: String, + pub log_format: LogFormat, + pub rate_limit_enabled: bool, + pub rate_limit_window_seconds: u64, + pub rate_limit_max_connections: usize, + pub rate_limit_block_threshold: usize, + pub rate_limit_slow_response: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + listen_addr: "0.0.0.0:8888".to_string(), + metrics_addr: "0.0.0.0:9100".to_string(), + disable_metrics: false, + backend_addr: "127.0.0.1:80".to_string(), + min_delay: 1000, + max_delay: 15000, + max_tarpit_time: 600, + block_threshold: 3, + trap_patterns: vec![ + // Basic attack patterns as plain strings + TrapPattern::as_plain("/vendor/phpunit"), + TrapPattern::as_plain("eval-stdin.php"), + TrapPattern::as_plain("/wp-admin"), + TrapPattern::as_plain("/wp-login.php"), + TrapPattern::as_plain("/xmlrpc.php"), + TrapPattern::as_plain("/phpMyAdmin"), + TrapPattern::as_plain("/solr/"), + TrapPattern::as_plain("/.env"), + TrapPattern::as_plain("/config"), + TrapPattern::as_plain("/actuator/"), + // More aggressive patterns for various PHP exploits. + // XXX: I dedicate this entire section to that one single crawler + // that has been scanning my entire network, hitting 403s left and right + // but not giving up, and coming back the next day at the same time to + // scan the same paths over and over. Kudos to you, random crawler. + TrapPattern::as_regex(r"/.*phpunit.*eval-stdin\.php"), + TrapPattern::as_regex(r"/index\.php\?s=/index/\\think\\app/invokefunction"), + TrapPattern::as_regex(r".*%ADd\+auto_prepend_file%3dphp://input.*"), + TrapPattern::as_regex(r".*%ADd\+allow_url_include%3d1.*"), + TrapPattern::as_regex(r".*/wp-content/plugins/.*\.php"), + TrapPattern::as_regex(r".*/wp-content/themes/.*\.php"), + TrapPattern::as_regex(r".*eval\(.*\).*"), + TrapPattern::as_regex(r".*/adminer\.php.*"), + TrapPattern::as_regex(r".*/admin\.php.*"), + TrapPattern::as_regex(r".*/administrator/.*"), + TrapPattern::as_regex(r".*/wp-json/.*"), + TrapPattern::as_regex(r".*/api/.*\.php.*"), + TrapPattern::as_regex(r".*/cgi-bin/.*"), + TrapPattern::as_regex(r".*/owa/.*"), + TrapPattern::as_regex(r".*/ecp/.*"), + TrapPattern::as_regex(r".*/webshell\.php.*"), + TrapPattern::as_regex(r".*/shell\.php.*"), + TrapPattern::as_regex(r".*/cmd\.php.*"), + TrapPattern::as_regex(r".*/struts.*"), + ], + whitelist_networks: vec![ + "192.168.0.0/16".to_string(), + "10.0.0.0/8".to_string(), + "172.16.0.0/12".to_string(), + "127.0.0.0/8".to_string(), + ], + markov_corpora_dir: "./corpora".to_string(), + lua_scripts_dir: "./scripts".to_string(), + data_dir: "./data".to_string(), + config_dir: "./conf".to_string(), + cache_dir: "./cache".to_string(), + log_format: LogFormat::Pretty, + rate_limit_enabled: true, + rate_limit_window_seconds: 60, + rate_limit_max_connections: 30, + rate_limit_block_threshold: 100, + rate_limit_slow_response: true, + } + } +} + +// Gets standard XDG directory paths for config, data and cache. +// XXX: This could be "simplified" by using the Dirs crate, but I can't +// really justify pulling a library for something I can handle in less +// than 30 lines. Unless cross-platform becomes necessary, the below +// implementation is good enough. For alternative platforms, we can simply +// enhance the current implementation as needed. +pub fn get_xdg_dirs() -> (PathBuf, PathBuf, PathBuf) { + let config_home = env::var_os("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); + home.join(".config") + }); + + let data_home = env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); + home.join(".local").join("share") + }); + + let cache_home = env::var_os("XDG_CACHE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| { + let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); + home.join(".cache") + }); + + let config_dir = config_home.join("eris"); + let data_dir = data_home.join("eris"); + let cache_dir = cache_home.join("eris"); + + (config_dir, data_dir, cache_dir) +} + +impl Config { + // Create configuration from command-line args. We'll be falling back to this + // when the configuration is invalid, so it must be validated more strictly. + pub fn from_args(args: &Args) -> Self { + let (config_dir, data_dir, cache_dir) = if let Some(base_dir) = &args.base_dir { + let base_str = base_dir.to_string_lossy().to_string(); + ( + format!("{base_str}/conf"), + format!("{base_str}/data"), + format!("{base_str}/cache"), + ) + } else { + let (c, d, cache) = get_xdg_dirs(); + ( + c.to_string_lossy().to_string(), + d.to_string_lossy().to_string(), + cache.to_string_lossy().to_string(), + ) + }; + + Self { + listen_addr: args.listen_addr.clone(), + metrics_addr: args.metrics_addr.clone(), + disable_metrics: args.disable_metrics, + backend_addr: args.backend_addr.clone(), + min_delay: args.min_delay, + max_delay: args.max_delay, + max_tarpit_time: args.max_tarpit_time, + block_threshold: args.block_threshold, + markov_corpora_dir: format!("{data_dir}/corpora"), + lua_scripts_dir: format!("{data_dir}/scripts"), + data_dir, + config_dir, + cache_dir, + log_format: LogFormat::Pretty, + rate_limit_enabled: args.rate_limit_enabled, + rate_limit_window_seconds: args.rate_limit_window, + rate_limit_max_connections: args.rate_limit_max, + rate_limit_block_threshold: args.rate_limit_block_threshold, + rate_limit_slow_response: args.rate_limit_slow_response, + ..Default::default() + } + } + + // Load configuration from a file (JSON or TOML) + pub fn load_from_file(path: &Path) -> std::io::Result { + let content = fs::read_to_string(path)?; + + let extension = path + .extension() + .map(|ext| ext.to_string_lossy().to_lowercase()) + .unwrap_or_default(); + + let config = match extension.as_str() { + "toml" => toml::from_str(&content).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to parse TOML: {e}"), + ) + })?, + _ => { + // Default to JSON for any other extension + serde_json::from_str(&content).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to parse JSON: {e}"), + ) + })? + } + }; + + Ok(config) + } + + // Save configuration to a file (JSON or TOML) + pub fn save_to_file(&self, path: &Path) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let extension = path + .extension() + .map(|ext| ext.to_string_lossy().to_lowercase()) + .unwrap_or_default(); + + let content = match extension.as_str() { + "toml" => toml::to_string_pretty(self).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to serialize to TOML: {e}"), + ) + })?, + _ => { + // Default to JSON for any other extension + serde_json::to_string_pretty(self).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Failed to serialize to JSON: {e}"), + ) + })? + } + }; + + fs::write(path, content)?; + Ok(()) + } + + // Create required directories if they don't exist + pub fn ensure_dirs_exist(&self) -> std::io::Result<()> { + let dirs = [ + &self.markov_corpora_dir, + &self.lua_scripts_dir, + &self.data_dir, + &self.config_dir, + &self.cache_dir, + ]; + + for dir in dirs { + fs::create_dir_all(dir)?; + log::debug!("Created directory: {dir}"); + } + + Ok(()) + } +} + +// Decide if a request should be tarpitted based on path and IP +pub fn should_tarpit(path: &str, ip: &IpAddr, config: &Config) -> bool { + // Check whitelist IPs first to avoid unnecessary pattern matching + for network_str in &config.whitelist_networks { + if let Ok(network) = network_str.parse::() { + if network.contains(*ip) { + return false; + } + } + } + + // Use pattern matching based on the trap pattern type. It can be + // a plain string or regex. + for pattern in &config.trap_patterns { + if pattern.matches(path) { + return true; + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr}; + + #[test] + fn test_config_from_args() { + let args = Args { + listen_addr: "127.0.0.1:8080".to_string(), + metrics_addr: "127.0.0.1:9000".to_string(), + disable_metrics: true, + backend_addr: "127.0.0.1:8081".to_string(), + min_delay: 500, + max_delay: 10000, + max_tarpit_time: 300, + block_threshold: 5, + base_dir: Some(PathBuf::from("/tmp/eris")), + config_file: None, + log_level: "debug".to_string(), + log_format: "pretty".to_string(), + rate_limit_enabled: true, + rate_limit_window: 30, + rate_limit_max: 20, + rate_limit_block_threshold: 50, + rate_limit_slow_response: true, + }; + + let config = Config::from_args(&args); + assert_eq!(config.listen_addr, "127.0.0.1:8080"); + assert_eq!(config.metrics_addr, "127.0.0.1:9000"); + assert!(config.disable_metrics); + assert_eq!(config.backend_addr, "127.0.0.1:8081"); + assert_eq!(config.min_delay, 500); + assert_eq!(config.max_delay, 10000); + assert_eq!(config.max_tarpit_time, 300); + assert_eq!(config.block_threshold, 5); + assert_eq!(config.markov_corpora_dir, "/tmp/eris/data/corpora"); + assert_eq!(config.lua_scripts_dir, "/tmp/eris/data/scripts"); + assert_eq!(config.data_dir, "/tmp/eris/data"); + assert_eq!(config.config_dir, "/tmp/eris/conf"); + assert_eq!(config.cache_dir, "/tmp/eris/cache"); + assert!(config.rate_limit_enabled); + assert_eq!(config.rate_limit_window_seconds, 30); + assert_eq!(config.rate_limit_max_connections, 20); + assert_eq!(config.rate_limit_block_threshold, 50); + assert!(config.rate_limit_slow_response); + } + + #[test] + fn test_trap_pattern_matching() { + // Test plain string pattern + let plain = TrapPattern::as_plain("phpunit"); + assert!(plain.matches("path/to/phpunit/test")); + assert!(!plain.matches("path/to/something/else")); + + // Test regex pattern + let regex = TrapPattern::as_regex(r".*eval-stdin\.php.*"); + assert!(regex.matches("/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php")); + assert!(regex.matches("/tests/eval-stdin.php?param")); + assert!(!regex.matches("/normal/path")); + + // Test invalid regex pattern (should return false) + let invalid = TrapPattern::Regex { + pattern: "(invalid[regex".to_string(), + regex: true, + }; + assert!(!invalid.matches("anything")); + } + + #[tokio::test] + async fn test_should_tarpit() { + let config = Config::default(); + + // Test trap patterns + assert!(should_tarpit( + "/vendor/phpunit/whatever", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + assert!(should_tarpit( + "/wp-admin/login.php", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + assert!(should_tarpit( + "/.env", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + + // Test whitelist networks + assert!(!should_tarpit( + "/wp-admin/login.php", + &IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + &config + )); + assert!(!should_tarpit( + "/vendor/phpunit/whatever", + &IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), + &config + )); + + // Test legitimate paths + assert!(!should_tarpit( + "/index.html", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + assert!(!should_tarpit( + "/images/logo.png", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + + // Test regex patterns + assert!(should_tarpit( + "/index.php?s=/index/\\think\\app/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=Hello", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + + assert!(should_tarpit( + "/hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input", + &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + &config + )); + } + + #[test] + fn test_config_file_formats() { + // Create temporary JSON config file + let temp_dir = std::env::temp_dir(); + let json_path = temp_dir.join("temp_config.json"); + let toml_path = temp_dir.join("temp_config.toml"); + + let config = Config::default(); + + // Test JSON serialization and deserialization + config.save_to_file(&json_path).unwrap(); + let loaded_json = Config::load_from_file(&json_path).unwrap(); + assert_eq!(loaded_json.listen_addr, config.listen_addr); + assert_eq!(loaded_json.min_delay, config.min_delay); + assert_eq!(loaded_json.rate_limit_enabled, config.rate_limit_enabled); + assert_eq!( + loaded_json.rate_limit_max_connections, + config.rate_limit_max_connections + ); + + // Test TOML serialization and deserialization + config.save_to_file(&toml_path).unwrap(); + let loaded_toml = Config::load_from_file(&toml_path).unwrap(); + assert_eq!(loaded_toml.listen_addr, config.listen_addr); + assert_eq!(loaded_toml.min_delay, config.min_delay); + assert_eq!(loaded_toml.rate_limit_enabled, config.rate_limit_enabled); + assert_eq!( + loaded_toml.rate_limit_max_connections, + config.rate_limit_max_connections + ); + + // Clean up + let _ = std::fs::remove_file(json_path); + let _ = std::fs::remove_file(toml_path); + } +} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index c055435..0000000 --- a/src/error.rs +++ /dev/null @@ -1,281 +0,0 @@ -use std::io; - -/// Result type alias for the application -pub type Result = std::result::Result; - -/// Comprehensive error types for the Eris application -#[derive(thiserror::Error, Debug)] -pub enum ErisError { - /// Configuration-related errors - #[error("Configuration error: {message}")] - Config { message: String }, - - /// Network-related errors - #[error("Network error: {0}")] - Network(#[from] io::Error), - - /// HTTP parsing errors - #[error("HTTP parsing error: {message}")] - HttpParse { message: String }, - - /// Firewall operation errors - #[error("Firewall operation failed: {message}")] - Firewall { message: String }, - - /// Lua script execution errors - #[error("Lua script error: {message}")] - Lua { message: String }, - - /// Markov chain generation errors - #[error("Markov generation error: {message}")] - Markov { message: String }, - - /// Metrics collection errors - #[error("Metrics error: {message}")] - Metrics { message: String }, - - /// File system errors - #[error("File system error: {message}")] - FileSystem { message: String }, - - /// Validation errors - #[error("Validation error: {message}")] - Validation { message: String }, - - /// IP address parsing errors - #[error("Invalid IP address: {address}")] - InvalidIp { address: String }, - - /// Connection limit exceeded - #[error("Connection limit exceeded: {current}/{max}")] - ConnectionLimit { current: usize, max: usize }, - - /// Rate limiting errors - #[error("Rate limit exceeded for IP: {ip}")] - RateLimit { ip: String }, - - /// Timeout errors - #[error("Operation timed out: {operation}")] - Timeout { operation: String }, - - /// Permission errors - #[error("Permission denied: {operation}")] - Permission { operation: String }, - - /// Resource not found errors - #[error("Resource not found: {resource}")] - NotFound { resource: String }, - - /// Generic application errors - #[error("Application error: {message}")] - Application { message: String }, -} - -impl ErisError { - /// Create a new configuration error - pub fn config>(message: T) -> Self { - Self::Config { - message: message.into(), - } - } - - /// Create a new HTTP parsing error - pub fn http_parse>(message: T) -> Self { - Self::HttpParse { - message: message.into(), - } - } - - /// Create a new firewall error - pub fn firewall>(message: T) -> Self { - Self::Firewall { - message: message.into(), - } - } - - /// Create a new Lua script error - pub fn lua>(message: T) -> Self { - Self::Lua { - message: message.into(), - } - } - - /// Create a new Markov generation error - pub fn markov>(message: T) -> Self { - Self::Markov { - message: message.into(), - } - } - - /// Create a new metrics error - pub fn metrics>(message: T) -> Self { - Self::Metrics { - message: message.into(), - } - } - - /// Create a new file system error - pub fn filesystem>(message: T) -> Self { - Self::FileSystem { - message: message.into(), - } - } - - /// Create a new validation error - pub fn validation>(message: T) -> Self { - Self::Validation { - message: message.into(), - } - } - - /// Create a new invalid IP error - pub fn invalid_ip>(address: T) -> Self { - Self::InvalidIp { - address: address.into(), - } - } - - /// Create a new connection limit error - #[must_use] - pub const fn connection_limit(current: usize, max: usize) -> Self { - Self::ConnectionLimit { current, max } - } - - /// Create a new rate limit error - pub fn rate_limit>(ip: T) -> Self { - Self::RateLimit { ip: ip.into() } - } - - /// Create a new timeout error - pub fn timeout>(operation: T) -> Self { - Self::Timeout { - operation: operation.into(), - } - } - - /// Create a new permission error - pub fn permission>(operation: T) -> Self { - Self::Permission { - operation: operation.into(), - } - } - - /// Create a new not found error - pub fn not_found>(resource: T) -> Self { - Self::NotFound { - resource: resource.into(), - } - } - - /// Create a new application error - pub fn application>(message: T) -> Self { - Self::Application { - message: message.into(), - } - } - - /// Check if this is a retryable error - #[must_use] - pub const fn is_retryable(&self) -> bool { - matches!( - self, - Self::Network(_) - | Self::Timeout { .. } - | Self::ConnectionLimit { .. } - | Self::RateLimit { .. } - ) - } - - /// Check if this error should be logged at debug level - #[must_use] - pub const fn is_debug_level(&self) -> bool { - matches!( - self, - Self::Network(_) | Self::HttpParse { .. } | Self::RateLimit { .. } - ) - } - - /// Get error category for metrics - #[must_use] - pub const fn category(&self) -> &'static str { - match self { - Self::Config { .. } => "config", - Self::Network { .. } => "network", - Self::HttpParse { .. } => "http", - Self::Firewall { .. } => "firewall", - Self::Lua { .. } => "lua", - Self::Markov { .. } => "markov", - Self::Metrics { .. } => "metrics", - Self::FileSystem { .. } => "filesystem", - Self::Validation { .. } => "validation", - Self::InvalidIp { .. } => "network", - Self::ConnectionLimit { .. } => "connection", - Self::RateLimit { .. } => "rate_limit", - Self::Timeout { .. } => "timeout", - Self::Permission { .. } => "permission", - Self::NotFound { .. } => "not_found", - Self::Application { .. } => "application", - } - } -} - -/// Convert from `serde_json::Error` -impl From for ErisError { - fn from(err: serde_json::Error) -> Self { - Self::config(format!("JSON parsing error: {err}")) - } -} - -/// Convert from `rlua::Error` -impl From for ErisError { - fn from(err: rlua::Error) -> Self { - Self::lua(format!("Lua execution error: {err}")) - } -} - -/// Convert from `ipnetwork::IpNetworkError` -impl From for ErisError { - fn from(err: ipnetwork::IpNetworkError) -> Self { - Self::validation(format!("IP network error: {err}")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_error_creation() { - let err = ErisError::config("Invalid port"); - assert!(matches!(err, ErisError::Config { .. })); - assert_eq!(err.category(), "config"); - } - - #[test] - fn test_error_retryable() { - assert!( - ErisError::Network(io::Error::new(io::ErrorKind::TimedOut, "timeout")).is_retryable() - ); - assert!(!ErisError::config("test").is_retryable()); - } - - #[test] - fn test_error_debug_level() { - assert!( - ErisError::Network(io::Error::new(io::ErrorKind::ConnectionRefused, "refused")) - .is_debug_level() - ); - assert!(!ErisError::config("test").is_debug_level()); - } - - #[test] - fn test_error_conversions() { - let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); - let eris_err: ErisError = io_err.into(); - assert!(matches!(eris_err, ErisError::Network(_))); - - let json_err = serde_json::from_str::("invalid json").unwrap_err(); - let eris_err: ErisError = json_err.into(); - assert!(matches!(eris_err, ErisError::Config { .. })); - } -} diff --git a/src/firewall.rs b/src/firewall.rs new file mode 100644 index 0000000..73f36e5 --- /dev/null +++ b/src/firewall.rs @@ -0,0 +1,128 @@ +use tokio::process::Command; + +// Set up nftables firewall rules for IP blocking +pub async fn setup_firewall() -> Result<(), String> { + log::info!("Setting up firewall rules"); + + // Check if nft command exists + let nft_exists = Command::new("which") + .arg("nft") + .output() + .await + .map(|output| output.status.success()) + .unwrap_or(false); + + if !nft_exists { + log::warn!("nft command not found. Firewall rules will not be set up."); + return Ok(()); + } + + // Create table if it doesn't exist + let output = Command::new("nft") + .args(["list", "table", "inet", "filter"]) + .output() + .await; + + match output { + Ok(output) => { + if !output.status.success() { + log::info!("Creating nftables table"); + let result = Command::new("nft") + .args(["create", "table", "inet", "filter"]) + .output() + .await; + + if let Err(e) = result { + return Err(format!("Failed to create nftables table: {e}")); + } + } + } + Err(e) => { + log::warn!("Failed to check if nftables table exists: {e}"); + log::info!("Will try to create it anyway"); + let result = Command::new("nft") + .args(["create", "table", "inet", "filter"]) + .output() + .await; + + if let Err(e) = result { + return Err(format!("Failed to create nftables table: {e}")); + } + } + } + + // Create blacklist set if it doesn't exist + let output = Command::new("nft") + .args(["list", "set", "inet", "filter", "eris_blacklist"]) + .output() + .await; + + match output { + Ok(output) => { + if !output.status.success() { + log::info!("Creating eris_blacklist set"); + let result = Command::new("nft") + .args([ + "create", + "set", + "inet", + "filter", + "eris_blacklist", + "{ type ipv4_addr; flags interval; }", + ]) + .output() + .await; + + if let Err(e) = result { + return Err(format!("Failed to create blacklist set: {e}")); + } + } + } + Err(e) => { + log::warn!("Failed to check if blacklist set exists: {e}"); + return Err(format!("Failed to check if blacklist set exists: {e}")); + } + } + + // Add rule to drop traffic from blacklisted IPs + let output = Command::new("nft") + .args(["list", "chain", "inet", "filter", "input"]) + .output() + .await; + + // Check if our rule already exists + match output { + Ok(output) => { + let rule_exists = String::from_utf8_lossy(&output.stdout) + .contains("ip saddr @eris_blacklist counter drop"); + + if !rule_exists { + log::info!("Adding drop rule for blacklisted IPs"); + let result = Command::new("nft") + .args([ + "add", + "rule", + "inet", + "filter", + "input", + "ip saddr @eris_blacklist", + "counter", + "drop", + ]) + .output() + .await; + + if let Err(e) = result { + return Err(format!("Failed to add firewall rule: {e}")); + } + } + } + Err(e) => { + log::warn!("Failed to check if firewall rule exists: {e}"); + return Err(format!("Failed to check if firewall rule exists: {e}")); + } + } + + log::info!("Firewall setup complete"); + Ok(()) +} diff --git a/src/lua/mod.rs b/src/lua/mod.rs new file mode 100644 index 0000000..fc96382 --- /dev/null +++ b/src/lua/mod.rs @@ -0,0 +1,901 @@ +use rlua::{Function, Lua, Table, Value}; +use std::collections::HashMap; +use std::collections::hash_map::DefaultHasher; +use std::fs; +use std::hash::{Hash, Hasher}; +use std::path::Path; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +// Event types for the Lua scripting system +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EventType { + Connection, // when a new connection is established + Request, // when a request is received + ResponseGen, // when generating a response + ResponseChunk, // before sending each response chunk + Disconnection, // when a connection is closed + BlockIP, // when an IP is being considered for blocking + Startup, // when the application starts + Shutdown, // when the application is shutting down + Periodic, // called periodically (e.g., every minute) +} + +impl EventType { + /// Convert event type to string representation for Lua + const fn as_str(&self) -> &'static str { + match self { + Self::Connection => "connection", + Self::Request => "request", + Self::ResponseGen => "response_gen", + Self::ResponseChunk => "response_chunk", + Self::Disconnection => "disconnection", + Self::BlockIP => "block_ip", + Self::Startup => "startup", + Self::Shutdown => "shutdown", + Self::Periodic => "periodic", + } + } + + /// Convert from string to `EventType` + fn from_str(s: &str) -> Option { + match s { + "connection" => Some(Self::Connection), + "request" => Some(Self::Request), + "response_gen" => Some(Self::ResponseGen), + "response_chunk" => Some(Self::ResponseChunk), + "disconnection" => Some(Self::Disconnection), + "block_ip" => Some(Self::BlockIP), + "startup" => Some(Self::Startup), + "shutdown" => Some(Self::Shutdown), + "periodic" => Some(Self::Periodic), + _ => None, + } + } +} + +// Loaded Lua script with its metadata +struct ScriptInfo { + name: String, + enabled: bool, +} + +// Script state and manage the Lua environment +pub struct ScriptManager { + lua: Mutex, + scripts: Vec, + hooks: HashMap>, + state: Arc>>, + counters: Arc>>, +} + +// Context passed to Lua event handlers +pub struct EventContext { + pub event_type: EventType, + pub ip: Option, + pub path: Option, + pub user_agent: Option, + pub request_headers: Option>, + pub content: Option, + pub timestamp: u64, + pub session_id: Option, +} + +// Make ScriptManager explicitly Send + Sync since we're using Mutex +unsafe impl Send for ScriptManager {} +unsafe impl Sync for ScriptManager {} + +impl ScriptManager { + /// Create a new script manager and load scripts from the given directory + pub fn new(scripts_dir: &str) -> Self { + let mut manager = Self { + lua: Mutex::new(Lua::new()), + scripts: Vec::new(), + hooks: HashMap::new(), + state: Arc::new(RwLock::new(HashMap::new())), + counters: Arc::new(RwLock::new(HashMap::new())), + }; + + // Initialize Lua environment with our API + manager.init_lua_env(); + + // Load scripts from directory + manager.load_scripts_from_dir(scripts_dir); + + // If no scripts were loaded, use default script + if manager.scripts.is_empty() { + log::info!("No Lua scripts found, loading default scripts"); + manager.load_script( + "default", + include_str!("../../resources/default_script.lua"), + ); + } + + // Trigger startup event + manager.trigger_event(&EventContext { + event_type: EventType::Startup, + ip: None, + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: None, + }); + + manager + } + + // Initialize the Lua environment + fn init_lua_env(&self) { + let state_clone = self.state.clone(); + let counters_clone = self.counters.clone(); + + if let Ok(lua) = self.lua.lock() { + // Create eris global table for our API + let eris_table = lua.create_table().unwrap(); + + self.register_utility_functions(&lua, &eris_table, state_clone, counters_clone); + self.register_event_functions(&lua, &eris_table); + self.register_logging_functions(&lua, &eris_table); + + // Set the eris global table + lua.globals().set("eris", eris_table).unwrap(); + } + } + + /// Register utility functions for scripts to use + fn register_utility_functions( + &self, + lua: &Lua, + eris_table: &Table, + state: Arc>>, + counters: Arc>>, + ) { + // Store a key-value pair in persistent state + let state_for_set = state.clone(); + let set_state = lua + .create_function(move |_, (key, value): (String, String)| { + let mut state_map = state_for_set.write().unwrap(); + state_map.insert(key, value); + Ok(()) + }) + .unwrap(); + eris_table.set("set_state", set_state).unwrap(); + + // Get a value from persistent state + let state_for_get = state; + let get_state = lua + .create_function(move |_, key: String| { + let state_map = state_for_get.read().unwrap(); + let value = state_map.get(&key).cloned(); + Ok(value) + }) + .unwrap(); + eris_table.set("get_state", get_state).unwrap(); + + // Increment a counter + let counters_for_inc = counters.clone(); + let inc_counter = lua + .create_function(move |_, (key, amount): (String, Option)| { + let mut counters_map = counters_for_inc.write().unwrap(); + let counter = counters_map.entry(key).or_insert(0); + *counter += amount.unwrap_or(1); + Ok(*counter) + }) + .unwrap(); + eris_table.set("inc_counter", inc_counter).unwrap(); + + // Get a counter value + let counters_for_get = counters; + let get_counter = lua + .create_function(move |_, key: String| { + let counters_map = counters_for_get.read().unwrap(); + let value = counters_map.get(&key).copied().unwrap_or(0); + Ok(value) + }) + .unwrap(); + eris_table.set("get_counter", get_counter).unwrap(); + + // Generate a random token/string + let gen_token = lua + .create_function(move |_, prefix: Option| { + let now = get_timestamp(); + let random = rand::random::(); + let token = format!("{}{:x}{:x}", prefix.unwrap_or_default(), now, random); + Ok(token) + }) + .unwrap(); + eris_table.set("gen_token", gen_token).unwrap(); + + // Get current timestamp + let timestamp = lua + .create_function(move |_, ()| Ok(get_timestamp())) + .unwrap(); + eris_table.set("timestamp", timestamp).unwrap(); + } + + // Register event handling functions + fn register_event_functions(&self, lua: &Lua, eris_table: &Table) { + // Create a table to store event handlers + let handlers_table = lua.create_table().unwrap(); + eris_table.set("handlers", handlers_table).unwrap(); + + // Function for scripts to register event handlers + let on_fn = lua + .create_function(move |lua, (event_name, handler): (String, Function)| { + let globals = lua.globals(); + let eris: Table = globals.get("eris").unwrap(); + let handlers: Table = eris.get("handlers").unwrap(); + + // Get or create a table for this event type + let event_handlers: Table = if let Ok(table) = handlers.get(&*event_name) { + table + } else { + let new_table = lua.create_table().unwrap(); + handlers.set(&*event_name, new_table.clone()).unwrap(); + new_table + }; + + // Add the handler to the table + let next_index = event_handlers.len().unwrap() + 1; + event_handlers.set(next_index, handler).unwrap(); + + Ok(()) + }) + .unwrap(); + eris_table.set("on", on_fn).unwrap(); + } + + // Register logging functions + fn register_logging_functions(&self, lua: &Lua, eris_table: &Table) { + // Debug logging + let debug = lua + .create_function(|_, message: String| { + log::debug!("[Lua] {message}"); + Ok(()) + }) + .unwrap(); + eris_table.set("debug", debug).unwrap(); + + // Info logging + let info = lua + .create_function(|_, message: String| { + log::info!("[Lua] {message}"); + Ok(()) + }) + .unwrap(); + eris_table.set("info", info).unwrap(); + + // Warning logging + let warn = lua + .create_function(|_, message: String| { + log::warn!("[Lua] {message}"); + Ok(()) + }) + .unwrap(); + eris_table.set("warn", warn).unwrap(); + + // Error logging + let error = lua + .create_function(|_, message: String| { + log::error!("[Lua] {message}"); + Ok(()) + }) + .unwrap(); + eris_table.set("error", error).unwrap(); + } + + // Load all scripts from a directory + fn load_scripts_from_dir(&mut self, scripts_dir: &str) { + let script_dir = Path::new(scripts_dir); + if !script_dir.exists() { + log::warn!("Lua scripts directory does not exist: {scripts_dir}"); + return; + } + + log::debug!("Loading Lua scripts from directory: {scripts_dir}"); + if let Ok(entries) = fs::read_dir(script_dir) { + // Sort entries by filename to ensure consistent loading order + let mut sorted_entries: Vec<_> = entries.filter_map(Result::ok).collect(); + sorted_entries.sort_by_key(std::fs::DirEntry::path); + + for entry in sorted_entries { + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) == Some("lua") { + if let Ok(content) = fs::read_to_string(&path) { + let script_name = path + .file_stem() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + log::debug!("Loading Lua script: {} ({})", script_name, path.display()); + self.load_script(&script_name, &content); + } else { + log::warn!("Failed to read Lua script: {}", path.display()); + } + } + } + } + } + + // Load a single script and register its event handlers + fn load_script(&mut self, name: &str, content: &str) { + // Store script info + self.scripts.push(ScriptInfo { + name: name.to_string(), + enabled: true, + }); + + // Execute the script to register its event handlers + if let Ok(lua) = self.lua.lock() { + if let Err(e) = lua.load(content).set_name(name).exec() { + log::warn!("Error loading Lua script '{name}': {e}"); + return; + } + + // Collect registered event handlers + let globals = lua.globals(); + let eris: Table = match globals.get("eris") { + Ok(table) => table, + Err(_) => return, + }; + + let handlers: Table = match eris.get("handlers") { + Ok(table) => table, + Err(_) => return, + }; + + // Store the event handlers in our hooks map + let mut tmp: rlua::TablePairs<'_, String, Table<'_>> = + handlers.pairs::(); + 'l: loop { + if let Some(event_pair) = tmp.next() { + if let Ok((event_name, _)) = event_pair { + if let Some(event_type) = EventType::from_str(&event_name) { + self.hooks + .entry(event_type) + .or_default() + .push(name.to_string()); + } + } + } else { + break 'l; + } + } + + log::info!("Loaded Lua script '{name}' successfully"); + } + } + + /// Check if a script is enabled + fn is_script_enabled(&self, name: &str) -> bool { + self.scripts + .iter() + .find(|s| s.name == name) + .is_some_and(|s| s.enabled) + } + + /// Trigger an event, calling all registered handlers + pub fn trigger_event(&self, ctx: &EventContext) -> Option { + // Check if we have any handlers for this event + if !self.hooks.contains_key(&ctx.event_type) { + return ctx.content.clone(); + } + + // Build the event data table to pass to Lua handlers + let mut result = ctx.content.clone(); + + if let Ok(lua) = self.lua.lock() { + // Create the event context table + let event_ctx = lua.create_table().unwrap(); + + // Add all the context fields + event_ctx.set("event", ctx.event_type.as_str()).unwrap(); + if let Some(ip) = &ctx.ip { + event_ctx.set("ip", ip.clone()).unwrap(); + } + if let Some(path) = &ctx.path { + event_ctx.set("path", path.clone()).unwrap(); + } + if let Some(ua) = &ctx.user_agent { + event_ctx.set("user_agent", ua.clone()).unwrap(); + } + event_ctx.set("timestamp", ctx.timestamp).unwrap(); + if let Some(sid) = &ctx.session_id { + event_ctx.set("session_id", sid.clone()).unwrap(); + } + + // Add request headers if available + if let Some(headers) = &ctx.request_headers { + let headers_table = lua.create_table().unwrap(); + for (key, value) in headers { + headers_table + .set(key.to_string(), value.to_string()) + .unwrap(); + } + event_ctx.set("headers", headers_table).unwrap(); + } + + // Add content if available + if let Some(content) = &ctx.content { + event_ctx.set("content", content.clone()).unwrap(); + } + + // Call all registered handlers for this event + if let Some(handler_scripts) = self.hooks.get(&ctx.event_type) { + for script_name in handler_scripts { + // Skip disabled scripts + if !self.is_script_enabled(script_name) { + continue; + } + + // Get the globals and handlers table + let globals = lua.globals(); + let eris: Table = match globals.get("eris") { + Ok(table) => table, + Err(_) => continue, + }; + + let handlers: Table = match eris.get("handlers") { + Ok(table) => table, + Err(_) => continue, + }; + + // Get handlers for this event + let event_handlers: Table = match handlers.get(ctx.event_type.as_str()) { + Ok(table) => table, + Err(_) => continue, + }; + + // Call each handler + for pair in event_handlers.pairs::() { + if let Ok((_, handler)) = pair { + let handler_result: rlua::Result> = + handler.call((event_ctx.clone(),)); + if let Ok(Some(new_content)) = handler_result { + // For response events, allow handlers to modify the content + if matches!( + ctx.event_type, + EventType::ResponseGen | EventType::ResponseChunk + ) { + result = Some(new_content); + } + } + } + } + } + } + } + + result + } + + /// Generate a deceptive response, calling all `response_gen` handlers + pub fn generate_response( + &self, + path: &str, + user_agent: &str, + ip: &str, + headers: &HashMap, + markov_text: &str, + ) -> String { + // Create event context + let ctx = EventContext { + event_type: EventType::ResponseGen, + ip: Some(ip.to_string()), + path: Some(path.to_string()), + user_agent: Some(user_agent.to_string()), + request_headers: Some(headers.clone()), + content: Some(markov_text.to_string()), + timestamp: get_timestamp(), + session_id: Some(generate_session_id(ip, user_agent)), + }; + + /// Trigger the event and get the modified content + self.trigger_event(&ctx).unwrap_or_else(|| { + // Fallback to maintain backward compatibility + self.expand_response( + markov_text, + "generic", + path, + &generate_session_id(ip, user_agent), + ) + }) + } + + /// Process a chunk before sending it to client + pub fn process_chunk(&self, chunk: &str, ip: &str, session_id: &str) -> String { + let ctx = EventContext { + event_type: EventType::ResponseChunk, + ip: Some(ip.to_string()), + path: None, + user_agent: None, + request_headers: None, + content: Some(chunk.to_string()), + timestamp: get_timestamp(), + session_id: Some(session_id.to_string()), + }; + + self.trigger_event(&ctx) + .unwrap_or_else(|| chunk.to_string()) + } + + /// Called when a connection is established + pub fn on_connection(&self, ip: &str) -> bool { + let ctx = EventContext { + event_type: EventType::Connection, + ip: Some(ip.to_string()), + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: None, + }; + + // If any handler returns false, reject the connection + let mut should_accept = true; + + if let Ok(lua) = self.lua.lock() { + if let Some(handler_scripts) = self.hooks.get(&EventType::Connection) { + for script_name in handler_scripts { + // Skip disabled scripts + if !self.is_script_enabled(script_name) { + continue; + } + + let globals = lua.globals(); + let eris: Table = match globals.get("eris") { + Ok(table) => table, + Err(_) => continue, + }; + + let handlers: Table = match eris.get("handlers") { + Ok(table) => table, + Err(_) => continue, + }; + + let event_handlers: Table = match handlers.get("connection") { + Ok(table) => table, + Err(_) => continue, + }; + + for pair in event_handlers.pairs::() { + if let Ok((_, handler)) = pair { + let event_ctx = create_event_context(&lua, &ctx); + if let Ok(result) = handler.call::<_, Value>((event_ctx,)) { + if result == Value::Boolean(false) { + should_accept = false; + break; + } + } + } + } + + if !should_accept { + break; + } + } + } + } + + should_accept + } + + /// Called when deciding whether to block an IP + pub fn should_block_ip(&self, ip: &str, hit_count: u32) -> bool { + let ctx = EventContext { + event_type: EventType::BlockIP, + ip: Some(ip.to_string()), + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: None, + }; + + // We should default to not modifying the blocking decision + let mut should_block = None; + + if let Ok(lua) = self.lua.lock() { + if let Some(handler_scripts) = self.hooks.get(&EventType::BlockIP) { + for script_name in handler_scripts { + // Skip disabled scripts + if !self.is_script_enabled(script_name) { + continue; + } + + let globals = lua.globals(); + let eris: Table = match globals.get("eris") { + Ok(table) => table, + Err(_) => continue, + }; + + let handlers: Table = match eris.get("handlers") { + Ok(table) => table, + Err(_) => continue, + }; + + let event_handlers: Table = match handlers.get("block_ip") { + Ok(table) => table, + Err(_) => continue, + }; + + for pair in event_handlers.pairs::() { + if let Ok((_, handler)) = pair { + let event_ctx = create_event_context(&lua, &ctx); + // Add hit count for the block_ip event + event_ctx.set("hit_count", hit_count).unwrap(); + + if let Ok(result) = handler.call::<_, Value>((event_ctx,)) { + if let Value::Boolean(block) = result { + should_block = Some(block); + break; + } + } + } + } + + if should_block.is_some() { + break; + } + } + } + } + + // Return the script's decision, or default to the system behavior + should_block.unwrap_or(hit_count >= 3) + } + + // Maintains backward compatibility with the old API + // XXX: I never liked expand_response, should probably be removeedf + // in the future. + pub fn expand_response( + &self, + text: &str, + response_type: &str, + path: &str, + token: &str, + ) -> String { + if let Ok(lua) = self.lua.lock() { + let globals = lua.globals(); + match globals.get::<_, Function>("enhance_response") { + Ok(enhance_func) => { + match enhance_func.call::<_, String>((text, response_type, path, token)) { + Ok(result) => result, + Err(e) => { + log::warn!("Error calling Lua function enhance_response: {e}"); + format!("{text}\n") + } + } + } + Err(_) => format!("{text}\n"), + } + } else { + format!("{text}\n") + } + } +} + +/// Get current timestamp in seconds +fn get_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +/// Create a unique session ID for tracking a connection +fn generate_session_id(ip: &str, user_agent: &str) -> String { + let timestamp = get_timestamp(); + let random = rand::random::(); + + // Use std::hash instead of xxhash_rust + let mut hasher = DefaultHasher::new(); + format!("{ip}_{user_agent}_{timestamp}").hash(&mut hasher); + let hash = hasher.finish(); + + format!("SID_{hash:x}_{random:x}") +} + +// Create an event context table in Lua +fn create_event_context<'a>(lua: &'a Lua, event_ctx: &EventContext) -> Table<'a> { + let table = lua.create_table().unwrap(); + + table.set("event", event_ctx.event_type.as_str()).unwrap(); + if let Some(ip) = &event_ctx.ip { + table.set("ip", ip.clone()).unwrap(); + } + if let Some(path) = &event_ctx.path { + table.set("path", path.clone()).unwrap(); + } + if let Some(ua) = &event_ctx.user_agent { + table.set("user_agent", ua.clone()).unwrap(); + } + table.set("timestamp", event_ctx.timestamp).unwrap(); + if let Some(sid) = &event_ctx.session_id { + table.set("session_id", sid.clone()).unwrap(); + } + if let Some(content) = &event_ctx.content { + table.set("content", content.clone()).unwrap(); + } + + table +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + use tempfile::TempDir; + + #[test] + fn test_event_registration() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("test_events.lua"); + let script_content = r#" + -- Example script with event handlers + eris.info("Registering event handlers") + + -- Connection event handler + eris.on("connection", function(ctx) + eris.debug("Connection from " .. ctx.ip) + return true -- accept the connection + end) + + -- Response generation handler + eris.on("response_gen", function(ctx) + eris.debug("Generating response for " .. ctx.path) + return ctx.content .. "" + end) + "#; + + fs::write(&script_path, script_content).unwrap(); + + let script_manager = ScriptManager::new(temp_dir.path().to_str().unwrap()); + + // Verify hooks were registered + assert!(script_manager.hooks.contains_key(&EventType::Connection)); + assert!(script_manager.hooks.contains_key(&EventType::ResponseGen)); + assert!(!script_manager.hooks.contains_key(&EventType::BlockIP)); + } + + #[test] + fn test_generate_response() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("response_test.lua"); + let script_content = r#" + eris.on("response_gen", function(ctx) + return ctx.content .. " - Modified by " .. ctx.user_agent + end) + "#; + + fs::write(&script_path, script_content).unwrap(); + + let script_manager = ScriptManager::new(temp_dir.path().to_str().unwrap()); + + let headers = HashMap::new(); + let result = script_manager.generate_response( + "/test/path", + "TestBot", + "127.0.0.1", + &headers, + "Original content", + ); + + assert!(result.contains("Original content")); + assert!(result.contains("Modified by TestBot")); + } + + #[test] + fn test_process_chunk() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("chunk_test.lua"); + let script_content = r#" + eris.on("response_chunk", function(ctx) + return ctx.content:gsub("secret", "REDACTED") + end) + "#; + + fs::write(&script_path, script_content).unwrap(); + + let script_manager = ScriptManager::new(temp_dir.path().to_str().unwrap()); + + let result = script_manager.process_chunk( + "This contains a secret password", + "127.0.0.1", + "test_session", + ); + + assert!(result.contains("This contains a REDACTED password")); + } + + #[test] + fn test_should_block_ip() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("block_test.lua"); + let script_content = r#" + eris.on("block_ip", function(ctx) + -- Block any IP with "192.168.1" prefix regardless of hit count + if string.match(ctx.ip, "^192%.168%.1%.") then + return true + end + + -- Don't block IPs with "10.0" prefix even if they hit the threshold + if string.match(ctx.ip, "^10%.0%.") then + return false + end + + -- Default behavior for other IPs (nil = use system default) + return nil + end) + "#; + + fs::write(&script_path, script_content).unwrap(); + + let script_manager = ScriptManager::new(temp_dir.path().to_str().unwrap()); + + // Should be blocked based on IP pattern + assert!(script_manager.should_block_ip("192.168.1.50", 1)); + + // Should not be blocked despite high hit count + assert!(!script_manager.should_block_ip("10.0.0.5", 10)); + + // Should use default behavior (block if >= 3 hits) + assert!(!script_manager.should_block_ip("172.16.0.1", 2)); + assert!(script_manager.should_block_ip("172.16.0.1", 3)); + } + + #[test] + fn test_state_and_counters() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("state_test.lua"); + let script_content = r#" + eris.on("startup", function(ctx) + eris.set_state("test_key", "test_value") + eris.inc_counter("visits", 0) + end) + + eris.on("connection", function(ctx) + local count = eris.inc_counter("visits") + eris.debug("Visit count: " .. count) + + -- Store last visitor + eris.set_state("last_visitor", ctx.ip) + return true + end) + + eris.on("response_gen", function(ctx) + local last_visitor = eris.get_state("last_visitor") or "unknown" + local visits = eris.get_counter("visits") + return ctx.content .. "" + end) + "#; + + fs::write(&script_path, script_content).unwrap(); + + let script_manager = ScriptManager::new(temp_dir.path().to_str().unwrap()); + + // Simulate connections + script_manager.on_connection("192.168.1.100"); + script_manager.on_connection("10.0.0.50"); + + // Check response includes state + let headers = HashMap::new(); + let result = script_manager.generate_response( + "/test", + "test-agent", + "8.8.8.8", + &headers, + "Response", + ); + + assert!(result.contains("Last visitor: 10.0.0.50")); + assert!(result.contains("Total visits: 2")); + } +} diff --git a/src/main.rs b/src/main.rs index 956ff69..c8d80a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,964 +1,106 @@ use actix_web::{App, HttpResponse, HttpServer, web}; use clap::Parser; -use eris::{BotState, ErisError, MarkovGenerator, Result}; -use ipnetwork::IpNetwork; -use rlua::{Function, Lua}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::env; use std::fs; - -use std::net::IpAddr; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::process::Command; +use std::time::Duration; +use tokio::net::TcpListener; use tokio::sync::RwLock; -use tokio::time::sleep; -// Import metrics from the metrics module -use eris::{ - ACTIVE_CONNECTIONS, BLOCKED_IPS, HITS_COUNTER, PATH_HITS, UA_HITS, metrics_handler, - status_handler, -}; - -// Command-line arguments using clap -#[derive(Parser, Debug, Clone)] -#[clap( - author, - version, - about = "Markov chain based HTTP tarpit/honeypot that delays and tracks potential attackers" -)] -struct Args { - #[clap( - long, - default_value = "0.0.0.0:8888", - help = "Address and port to listen for incoming HTTP requests (format: ip:port)" - )] - listen_addr: String, - - #[clap( - long, - default_value = "0.0.0.0:9100", - help = "Address and port to expose Prometheus metrics and status endpoint (format: ip:port)" - )] - metrics_addr: String, - - #[clap(long, help = "Disable Prometheus metrics server completely")] - disable_metrics: bool, - - #[clap( - long, - default_value = "127.0.0.1:80", - help = "Backend server address to proxy legitimate requests to (format: ip:port)" - )] - backend_addr: String, - - #[clap( - long, - default_value = "1000", - help = "Minimum delay in milliseconds between chunks sent to attacker" - )] - min_delay: u64, - - #[clap( - long, - default_value = "15000", - help = "Maximum delay in milliseconds between chunks sent to attacker" - )] - max_delay: u64, - - #[clap( - long, - default_value = "600", - help = "Maximum time in seconds to keep an attacker in the tarpit before disconnecting" - )] - max_tarpit_time: u64, - - #[clap( - long, - default_value = "3", - help = "Number of hits to honeypot patterns before permanently blocking an IP" - )] - block_threshold: u32, - - #[clap( - long, - help = "Base directory for all application data (overrides XDG directory structure)" - )] - base_dir: Option, - - #[clap( - long, - help = "Path to JSON configuration file (overrides command line options)" - )] - config_file: Option, - - #[clap( - long, - default_value = "info", - help = "Log level: trace, debug, info, warn, error" - )] - log_level: String, -} - -// Configuration structure -#[derive(Clone, Debug, Deserialize, Serialize)] -struct Config { - listen_addr: String, - metrics_addr: String, - disable_metrics: bool, - backend_addr: String, - min_delay: u64, - max_delay: u64, - max_tarpit_time: u64, - block_threshold: u32, - trap_patterns: Vec, - whitelist_networks: Vec, - markov_corpora_dir: String, - lua_scripts_dir: String, - data_dir: String, - config_dir: String, - cache_dir: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - listen_addr: "0.0.0.0:8888".to_string(), - metrics_addr: "0.0.0.0:9100".to_string(), - disable_metrics: false, - backend_addr: "127.0.0.1:80".to_string(), - min_delay: 1000, - max_delay: 15000, - max_tarpit_time: 600, - block_threshold: 3, - trap_patterns: vec![ - "/vendor/phpunit".to_string(), - "eval-stdin.php".to_string(), - "/wp-admin".to_string(), - "/wp-login.php".to_string(), - "/xmlrpc.php".to_string(), - "/phpMyAdmin".to_string(), - "/solr/".to_string(), - "/.env".to_string(), - "/config".to_string(), - "/api/".to_string(), - "/actuator/".to_string(), - "/search/feedback".to_string(), - "/wp-json/v1/u".to_string(), - ], - whitelist_networks: vec![ - "192.168.0.0/16".to_string(), - "10.0.0.0/8".to_string(), - "172.16.0.0/12".to_string(), - "127.0.0.0/8".to_string(), - ], - markov_corpora_dir: "./corpora".to_string(), - lua_scripts_dir: "./scripts".to_string(), - data_dir: "./data".to_string(), - config_dir: "./conf".to_string(), - cache_dir: "./cache".to_string(), - } - } -} - -// Gets standard XDG directory paths for config, data and cache -fn get_xdg_dirs() -> (PathBuf, PathBuf, PathBuf) { - let config_home = env::var_os("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| { - let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); - home.join(".config") - }); - - let data_home = env::var_os("XDG_DATA_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| { - let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); - home.join(".local").join("share") - }); - - let cache_home = env::var_os("XDG_CACHE_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| { - let home = env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from); - home.join(".cache") - }); - - let config_dir = config_home.join("eris"); - let data_dir = data_home.join("eris"); - let cache_dir = cache_home.join("eris"); - - (config_dir, data_dir, cache_dir) -} - -impl Config { - // Create configuration from command-line args - fn from_args(args: &Args) -> Self { - let (config_dir, data_dir, cache_dir) = if let Some(base_dir) = &args.base_dir { - let base_str = base_dir.to_string_lossy().to_string(); - ( - format!("{base_str}/conf"), - format!("{base_str}/data"), - format!("{base_str}/cache"), - ) - } else { - let (c, d, cache) = get_xdg_dirs(); - ( - c.to_string_lossy().to_string(), - d.to_string_lossy().to_string(), - cache.to_string_lossy().to_string(), - ) - }; - - Self { - listen_addr: args.listen_addr.clone(), - metrics_addr: args.metrics_addr.clone(), - disable_metrics: args.disable_metrics, - backend_addr: args.backend_addr.clone(), - min_delay: args.min_delay, - max_delay: args.max_delay, - max_tarpit_time: args.max_tarpit_time, - block_threshold: args.block_threshold, - markov_corpora_dir: format!("{data_dir}/corpora"), - lua_scripts_dir: format!("{data_dir}/scripts"), - data_dir, - config_dir, - cache_dir, - ..Default::default() - } - } - - // Load configuration from a JSON file - fn load_from_file(path: &Path) -> std::io::Result { - let content = fs::read_to_string(path)?; - let config = serde_json::from_str(&content)?; - Ok(config) - } - - // Save configuration to a JSON file - fn save_to_file(&self, path: &Path) -> std::io::Result<()> { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent)?; - } - let content = serde_json::to_string_pretty(self)?; - fs::write(path, content)?; - Ok(()) - } - - // Create required directories if they don't exist - fn ensure_dirs_exist(&self) -> std::io::Result<()> { - let dirs = [ - &self.markov_corpora_dir, - &self.lua_scripts_dir, - &self.data_dir, - &self.config_dir, - &self.cache_dir, - ]; - - for dir in dirs { - fs::create_dir_all(dir)?; - log::debug!("Created directory: {dir}"); - } - - Ok(()) - } -} - -// Lua scripts for response generation and customization -struct ScriptManager { - script_content: String, - scripts_loaded: bool, -} - -impl ScriptManager { - fn new(scripts_dir: &str) -> Self { - let mut script_content = String::new(); - let mut scripts_loaded = false; - - // Try to load scripts from directory - let script_dir = Path::new(scripts_dir); - if script_dir.exists() { - log::debug!("Loading Lua scripts from directory: {scripts_dir}"); - if let Ok(entries) = fs::read_dir(script_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|ext| ext.to_str()) == Some("lua") { - if let Ok(content) = fs::read_to_string(&path) { - log::debug!("Loaded Lua script: {}", path.display()); - script_content.push_str(&content); - script_content.push('\n'); - scripts_loaded = true; - } else { - log::warn!("Failed to read Lua script: {}", path.display()); - } - } - } - } - } else { - log::warn!("Lua scripts directory does not exist: {scripts_dir}"); - } - - // If no scripts were loaded, use a default script - if !scripts_loaded { - log::info!("No Lua scripts found, loading default scripts"); - script_content = r#" - function generate_honeytoken(token) - local token_types = {"API_KEY", "AUTH_TOKEN", "SESSION_ID", "SECRET_KEY"} - local prefix = token_types[math.random(#token_types)] - local suffix = string.format("%08x", math.random(0xffffff)) - return prefix .. "_" .. token .. "_" .. suffix - end - - function enhance_response(text, response_type, path, token) - local result = text - local honeytoken = generate_honeytoken(token) - - -- Add some fake sensitive data - result = result .. "\n" - result = result .. "\n
Server ID: " .. token .. "
" - - return result - end - "# - .to_string(); - scripts_loaded = true; - } - - Self { - script_content, - scripts_loaded, - } - } - - // Lua is a powerful configuration language we can use to expand functionality of - // Eris, e.g., with fake tokens or honeytrap content. - fn expand_response(&self, text: &str, response_type: &str, path: &str, token: &str) -> String { - if !self.scripts_loaded { - return format!("{text}\n"); - } - - let lua = Lua::new(); - if let Err(e) = lua.load(&self.script_content).exec() { - log::warn!("Error loading Lua script: {e}"); - return format!("{text}\n"); - } - - let globals = lua.globals(); - match globals.get::<_, Function>("enhance_response") { - Ok(enhance_func) => { - match enhance_func.call::<_, String>((text, response_type, path, token)) { - Ok(result) => result, - Err(e) => { - log::warn!("Error calling Lua function enhance_response: {e}"); - format!("{text}\n") - } - } - } - Err(e) => { - log::warn!("Lua enhance_response function not found: {e}"); - format!("{text}\n") - } - } - } -} - -// Main connection handler - decides whether to tarpit or proxy -async fn handle_connection( - mut stream: TcpStream, - config: Arc, - state: Arc>, - markov_generator: Arc, - script_manager: Arc, -) { - let peer_addr = match stream.peer_addr() { - Ok(addr) => addr.ip(), - Err(e) => { - log::debug!("Failed to get peer address: {e}"); - return; - } - }; - - log::debug!("New connection from: {peer_addr}"); - - // Check if IP is already blocked - if state.read().await.blocked.contains(&peer_addr) { - log::debug!("Rejected connection from blocked IP: {peer_addr}"); - let _ = stream.shutdown().await; - return; - } - - // Read the HTTP request - let mut buffer = [0; 8192]; - let mut request_data = Vec::new(); - - // Read with timeout to prevent hanging - let read_fut = async { - loop { - match stream.read(&mut buffer).await { - Ok(0) => break, - Ok(n) => { - request_data.extend_from_slice(&buffer[..n]); - // Stop reading at empty line, this is the end of HTTP headers - if request_data.len() > 2 && &request_data[request_data.len() - 2..] == b"\r\n" - { - break; - } - } - Err(e) => { - log::debug!("Error reading from stream: {e}"); - break; - } - } - } - }; - - let timeout_fut = sleep(Duration::from_secs(5)); - - tokio::select! { - () = read_fut => {}, - () = timeout_fut => { - log::debug!("Connection timeout from: {peer_addr}"); - let _ = stream.shutdown().await; - return; - } - } - - // Parse the request - let request_str = String::from_utf8_lossy(&request_data); - let request_lines: Vec<&str> = request_str.lines().collect(); - - if request_lines.is_empty() { - log::debug!("Empty request from: {peer_addr}"); - let _ = stream.shutdown().await; - return; - } - - // Parse request line - let request_parts: Vec<&str> = request_lines[0].split_whitespace().collect(); - if request_parts.len() < 3 { - log::debug!("Malformed request from {}: {}", peer_addr, request_lines[0]); - let _ = stream.shutdown().await; - return; - } - - let method = request_parts[0]; - let path = request_parts[1]; - let protocol = request_parts[2]; - - log::debug!("Request: {method} {path} {protocol} from {peer_addr}"); - - // Parse headers - let mut headers = HashMap::new(); - for line in &request_lines[1..] { - if line.is_empty() { - break; - } - - if let Some(idx) = line.find(':') { - let key = line[..idx].trim(); - let value = line[idx + 1..].trim(); - headers.insert(key, value.to_string()); - } - } - - let user_agent = headers - .get("user-agent") - .cloned() - .unwrap_or_else(|| "unknown".to_string()); - - // Check if this request matches our tarpit patterns - let should_tarpit = should_tarpit(path, &peer_addr, &config).await; - - if should_tarpit { - log::info!("Tarpit triggered: {method} {path} from {peer_addr} (UA: {user_agent})"); - - // Update metrics - HITS_COUNTER.inc(); - PATH_HITS.with_label_values(&[path]).inc(); - UA_HITS.with_label_values(&[&user_agent]).inc(); - - // Update state and check for blocking threshold - { - let mut state = state.write().await; - state.active_connections.insert(peer_addr); - ACTIVE_CONNECTIONS.set(state.active_connections.len() as f64); - - *state.hits.entry(peer_addr).or_insert(0) += 1; - let hit_count = state.hits[&peer_addr]; - log::debug!("Hit count for {peer_addr}: {hit_count}"); - - // Block IPs that hit tarpits too many times - if hit_count >= config.block_threshold && !state.blocked.contains(&peer_addr) { - log::info!("Blocking IP {peer_addr} after {hit_count} hits"); - state.blocked.insert(peer_addr); - BLOCKED_IPS.set(state.blocked.len() as f64); - state.save_to_disk(); - - // Try to add to firewall - let peer_addr_str = peer_addr.to_string(); - tokio::spawn(async move { - log::debug!("Adding IP {peer_addr_str} to firewall blacklist"); - match Command::new("nft") - .args([ - "add", - "element", - "inet", - "filter", - "eris_blacklist", - "{", - &peer_addr_str, - "}", - ]) - .output() - .await - { - Ok(output) => { - if !output.status.success() { - log::warn!( - "Failed to add IP {} to firewall: {}", - peer_addr_str, - String::from_utf8_lossy(&output.stderr) - ); - } - } - Err(e) => { - log::warn!("Failed to execute nft command: {e}"); - } - } - }); - } - } - - // Generate a deceptive response using Markov chains and Lua - let response = - generate_deceptive_response(path, &user_agent, &markov_generator, &script_manager) - .await; - - // Send the response with the tarpit delay strategy - tarpit_connection( - stream, - response, - peer_addr, - state.clone(), - config.min_delay, - config.max_delay, - config.max_tarpit_time, - ) - .await; - } else { - log::debug!("Proxying request: {method} {path} from {peer_addr}"); - - // Proxy non-matching requests to the actual backend - proxy_to_backend( - stream, - method, - path, - protocol, - &headers, - &config.backend_addr, - ) - .await; - } -} - -// Determine if a request should be tarpitted based on path and IP -async fn should_tarpit(path: &str, ip: &IpAddr, config: &Config) -> bool { - // Don't tarpit whitelisted IPs (internal networks, etc) - for network_str in &config.whitelist_networks { - if let Ok(network) = network_str.parse::() - && network.contains(*ip) { - log::debug!("IP {ip} is in whitelist network {network_str}"); - return false; - } - } - - // Check if the request path matches any of our trap patterns - for pattern in &config.trap_patterns { - if path.contains(pattern) { - log::debug!("Path '{path}' matches trap pattern '{pattern}'"); - return true; - } - } - - // No trap patterns matched - false -} - -// Generate a deceptive HTTP response that appears legitimate -async fn generate_deceptive_response( - path: &str, - user_agent: &str, - markov: &MarkovGenerator, - script_manager: &ScriptManager, -) -> String { - // Choose response type based on path to seem more realistic - let response_type = if path.contains("phpunit") || path.contains("eval") { - "php_exploit" - } else if path.contains("wp-") { - "wordpress" - } else if path.contains("api") { - "api" - } else { - "generic" - }; - - log::debug!("Generating {response_type} response for path: {path}"); - - // Generate tracking token for this interaction - let tracking_token = format!( - "BOT_{}_{}", - user_agent - .chars() - .filter(|c| c.is_alphanumeric()) - .collect::(), - chrono::Utc::now().timestamp() - ); - - // Generate base response using Markov chain text generator - let markov_text = markov.generate(response_type, 30); - - // Use Lua to enhance with honeytokens and other deceptive content - let enhanced = - script_manager.expand_response(&markov_text, response_type, path, &tracking_token); - - // Return full HTTP response with appropriate headers - format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Powered-By: PHP/7.4.3\r\nConnection: keep-alive\r\n\r\n{enhanced}" - ) -} - -// Slowly feed a response to the client with random delays to waste attacker time -async fn tarpit_connection( - mut stream: TcpStream, - response: String, - peer_addr: IpAddr, - state: Arc>, - min_delay: u64, - max_delay: u64, - max_tarpit_time: u64, -) { - let start_time = Instant::now(); - let mut chars = response.chars().collect::>(); - - // Randomize the char order slightly to confuse automated tools - for i in (0..chars.len()).rev() { - if i > 0 && rand::random::() < 0.1 { - chars.swap(i, i - 1); - } - } - - log::debug!( - "Starting tarpit for {} with {} chars, min_delay={}ms, max_delay={}ms", - peer_addr, - chars.len(), - min_delay, - max_delay - ); - - let mut position = 0; - let mut chunks_sent = 0; - let mut total_delay = 0; - - // Send the response character by character with random delays - while position < chars.len() { - // Check if we've exceeded maximum tarpit time - let elapsed_secs = start_time.elapsed().as_secs(); - if elapsed_secs > max_tarpit_time { - log::info!("Tarpit maximum time ({max_tarpit_time} sec) reached for {peer_addr}"); - break; - } - - // Decide how many chars to send in this chunk (usually 1, sometimes more) - let chunk_size = if rand::random::() < 0.9 { - 1 - } else { - (rand::random::() * 3.0).floor() as usize + 1 - }; - - let end = (position + chunk_size).min(chars.len()); - let chunk: String = chars[position..end].iter().collect(); - - // Try to write chunk - if stream.write_all(chunk.as_bytes()).await.is_err() { - log::debug!("Connection closed by client during tarpit: {peer_addr}"); - break; - } - - if stream.flush().await.is_err() { - log::debug!("Failed to flush stream during tarpit: {peer_addr}"); - break; - } - - position = end; - chunks_sent += 1; - - // Apply random delay between min and max configured values - let delay_ms = (rand::random::() * (max_delay - min_delay) as f32) as u64 + min_delay; - total_delay += delay_ms; - sleep(Duration::from_millis(delay_ms)).await; - } - - log::debug!( - "Tarpit stats for {}: sent {} chunks, {}% of data, total delay {}ms over {}s", - peer_addr, - chunks_sent, - position * 100 / chars.len(), - total_delay, - start_time.elapsed().as_secs() - ); - - // Remove from active connections - if let Ok(mut state) = state.try_write() { - state.active_connections.remove(&peer_addr); - ACTIVE_CONNECTIONS.set(state.active_connections.len() as f64); - } - - let _ = stream.shutdown().await; -} - -// Forward a legitimate request to the real backend server -async fn proxy_to_backend( - mut client_stream: TcpStream, - method: &str, - path: &str, - protocol: &str, - headers: &HashMap<&str, String>, - backend_addr: &str, -) { - // Connect to backend server - let server_stream = match TcpStream::connect(backend_addr).await { - Ok(stream) => stream, - Err(e) => { - log::warn!("Failed to connect to backend {backend_addr}: {e}"); - let _ = client_stream.shutdown().await; - return; - } - }; - - log::debug!("Connected to backend server at {backend_addr}"); - - // Forward the original request - let mut request = format!("{method} {path} {protocol}\r\n"); - for (key, value) in headers { - request.push_str(&format!("{key}: {value}\r\n")); - } - request.push_str("\r\n"); - - let mut server_stream = server_stream; - if server_stream.write_all(request.as_bytes()).await.is_err() { - log::debug!("Failed to write request to backend server"); - let _ = client_stream.shutdown().await; - return; - } - - // Set up bidirectional forwarding between client and backend - let (mut client_read, mut client_write) = client_stream.split(); - let (mut server_read, mut server_write) = server_stream.split(); - - // Client -> Server - let client_to_server = async { - let mut buf = [0; 8192]; - let mut bytes_forwarded = 0; - - loop { - match client_read.read(&mut buf).await { - Ok(0) => break, - Ok(n) => { - bytes_forwarded += n; - if server_write.write_all(&buf[..n]).await.is_err() { - break; - } - } - Err(_) => break, - } - } - - log::debug!("Client -> Server: forwarded {bytes_forwarded} bytes"); - }; - - // Server -> Client - let server_to_client = async { - let mut buf = [0; 8192]; - let mut bytes_forwarded = 0; - - loop { - match server_read.read(&mut buf).await { - Ok(0) => break, - Ok(n) => { - bytes_forwarded += n; - if client_write.write_all(&buf[..n]).await.is_err() { - break; - } - } - Err(_) => break, - } - } - - log::debug!("Server -> Client: forwarded {bytes_forwarded} bytes"); - }; - - // Run both directions concurrently - tokio::select! { - () = client_to_server => {}, - () = server_to_client => {}, - } - - log::debug!("Proxy connection completed"); -} - -// Set up nftables firewall rules for IP blocking -async fn setup_firewall() -> Result<()> { - log::info!("Setting up firewall rules"); - - // Check if nft command exists - let nft_exists = Command::new("which") - .arg("nft") - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false); - - if !nft_exists { - log::warn!("nft command not found. Firewall rules will not be set up."); - return Ok(()); - } - - // Create table if it doesn't exist - let output = Command::new("nft") - .args(["list", "table", "inet", "filter"]) - .output() - .await; - - match output { - Ok(output) => { - if !output.status.success() { - log::info!("Creating nftables table"); - let result = Command::new("nft") - .args(["create", "table", "inet", "filter"]) - .output() - .await; - - if let Err(e) = result { - log::error!("Failed to create nftables table: {e}"); - return Err(ErisError::firewall("Failed to create nftables table")); - } - } - } - Err(e) => { - log::warn!("Failed to check if nftables table exists: {e}"); - log::info!("Will try to create it anyway"); - let result = Command::new("nft") - .args(["create", "table", "inet", "filter"]) - .output() - .await; - - if let Err(e) = result { - log::error!("Failed to create nftables table: {e}"); - return Err(ErisError::firewall("Failed to create nftables table")); - } - } - } - - // Create blacklist set if it doesn't exist - let output = Command::new("nft") - .args(["list", "set", "inet", "filter", "eris_blacklist"]) - .output() - .await; - - match output { - Ok(output) => { - if !output.status.success() { - log::info!("Creating eris_blacklist set"); - let result = Command::new("nft") - .args([ - "create", - "set", - "inet", - "filter", - "eris_blacklist", - "{ type ipv4_addr; flags interval; }", - ]) - .output() - .await; - - if let Err(e) = result { - log::error!("Failed to create blacklist set: {e}"); - return Err(ErisError::firewall("Failed to create blacklist set")); - } - } - } - Err(e) => { - log::error!("Failed to check if blacklist set exists: {e}"); - return Err(ErisError::firewall( - "Failed to check if blacklist set exists", - )); - } - } - - // Add rule to drop traffic from blacklisted IPs - let output = Command::new("nft") - .args(["list", "chain", "inet", "filter", "input"]) - .output() - .await; - - // Check if our rule already exists - match output { - Ok(output) => { - let rule_exists = String::from_utf8_lossy(&output.stdout) - .contains("ip saddr @eris_blacklist counter drop"); - - if !rule_exists { - log::info!("Adding drop rule for blacklisted IPs"); - let result = Command::new("nft") - .args([ - "add", - "rule", - "inet", - "filter", - "input", - "ip saddr @eris_blacklist", - "counter", - "drop", - ]) - .output() - .await; - - if let Err(e) = result { - log::error!("Failed to add firewall rule: {e}"); - return Err(ErisError::firewall("Failed to add firewall rule")); - } - } - } - Err(e) => { - log::error!("Failed to check if firewall rule exists: {e}"); - return Err(ErisError::firewall( - "Failed to check if firewall rule exists", - )); - } - } - - log::info!("Firewall setup complete"); - Ok(()) -} +mod config; +mod firewall; +mod lua; +mod markov; +mod metrics; +mod network; +mod state; +mod utils; + +use config::{Args, Config}; +use lua::{EventContext, EventType, ScriptManager}; +use markov::MarkovGenerator; +use metrics::{metrics_handler, status_handler}; +use network::handle_connection; +use state::BotState; +use utils::get_timestamp; #[actix_web::main] async fn main() -> std::io::Result<()> { // Parse command line arguments let args = Args::parse(); - // Initialize the logger - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(&args.log_level)) - .format_timestamp_millis() - .init(); + // Determine log format from args + let log_format = match args.log_format.to_lowercase().as_str() { + "json" => config::LogFormat::Json, + "pretty-json" => config::LogFormat::PrettyJson, + "plain" => config::LogFormat::Plain, + _ => config::LogFormat::Pretty, + }; - log::info!("Starting eris tarpit system"); + // Initialize the logger with proper formatting + let env = env_logger::Env::default().default_filter_or(&args.log_level); + let mut builder = env_logger::Builder::from_env(env); + + match log_format { + config::LogFormat::Plain => { + builder.format_timestamp_millis().init(); + } + config::LogFormat::Pretty => { + builder + .format(|buf, record| { + use std::io::Write; + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + writeln!( + buf, + "[{}] {} [{}] {}", + timestamp, + record.level(), + record.target(), + record.args() + ) + }) + .init(); + } + config::LogFormat::Json => { + builder + .format(|buf, record| { + use std::io::Write; + let json = serde_json::json!({ + "timestamp": chrono::Local::now().to_rfc3339(), + "level": record.level().to_string(), + "target": record.target(), + "message": record.args().to_string(), + "module_path": record.module_path(), + "file": record.file(), + "line": record.line(), + }); + writeln!(buf, "{}", json) + }) + .init(); + } + config::LogFormat::PrettyJson => { + builder + .format(|buf, record| { + use std::io::Write; + let json = serde_json::json!({ + "timestamp": chrono::Local::now().to_rfc3339(), + "level": record.level().to_string(), + "target": record.target(), + "message": record.args().to_string(), + "module_path": record.module_path(), + "file": record.file(), + "line": record.line(), + }); + writeln!(buf, "{}", serde_json::to_string_pretty(&json).unwrap()) + }) + .init(); + } + } + + log::info!("Starting Eris tarpit system"); // Load configuration - let config = if let Some(config_path) = &args.config_file { + let mut config = if let Some(config_path) = &args.config_file { log::info!("Loading configuration from {config_path:?}"); match Config::load_from_file(config_path) { Ok(cfg) => { @@ -976,6 +118,11 @@ async fn main() -> std::io::Result<()> { Config::from_args(&args) }; + // Log format from the command line needs to be preserved + if args.config_file.is_none() { + config.log_format = log_format; + } + // Ensure required directories exist match config.ensure_dirs_exist() { Ok(()) => log::info!("Directory setup completed"), @@ -990,12 +137,23 @@ async fn main() -> std::io::Result<()> { if let Err(e) = fs::create_dir_all(&config.config_dir) { log::warn!("Failed to create config directory: {e}"); } else { - let config_path = Path::new(&config.config_dir).join("config.json"); - if !config_path.exists() { - if let Err(e) = config.save_to_file(&config_path) { - log::warn!("Failed to save default configuration: {e}"); + // Save both JSON and TOML versions of the config for user reference + let config_path_json = Path::new(&config.config_dir).join("config.json"); + let config_path_toml = Path::new(&config.config_dir).join("config.toml"); + + if !config_path_json.exists() { + if let Err(e) = config.save_to_file(&config_path_json) { + log::warn!("Failed to save JSON configuration: {e}"); } else { - log::info!("Saved default configuration to {config_path:?}"); + log::info!("Saved JSON configuration to {config_path_json:?}"); + } + } + + if !config_path_toml.exists() { + if let Err(e) = config.save_to_file(&config_path_toml) { + log::warn!("Failed to save TOML configuration: {e}"); + } else { + log::info!("Saved TOML configuration to {config_path_toml:?}"); } } } @@ -1011,7 +169,7 @@ async fn main() -> std::io::Result<()> { let config = Arc::new(config); // Setup firewall rules for IP blocking - match setup_firewall().await { + match firewall::setup_firewall().await { Ok(()) => {} Err(e) => { log::warn!("Failed to set up firewall rules: {e}"); @@ -1036,6 +194,8 @@ async fn main() -> std::io::Result<()> { // Initialize Lua script manager log::info!("Loading Lua scripts from {}", config.lua_scripts_dir); let script_manager = Arc::new(ScriptManager::new(&config.lua_scripts_dir)); + let script_manager_for_tarpit = script_manager.clone(); + let script_manager_for_periodic = script_manager.clone(); // Clone config for metrics server let metrics_config = config.clone(); @@ -1047,8 +207,7 @@ async fn main() -> std::io::Result<()> { let listener = match TcpListener::bind(&config.listen_addr).await { Ok(l) => l, Err(e) => { - log::error!("Failed to bind to {}: {}", config.listen_addr, e); - return Err(ErisError::config("Failed to bind to listen address")); + return Err(format!("Failed to bind to {}: {}", config.listen_addr, e)); } }; @@ -1061,7 +220,7 @@ async fn main() -> std::io::Result<()> { let state_clone = tarpit_state.clone(); let markov_clone = markov_generator.clone(); - let script_manager_clone = script_manager.clone(); + let script_manager_clone = script_manager_for_tarpit.clone(); let config_clone = config.clone(); tokio::spawn(async move { @@ -1082,7 +241,7 @@ async fn main() -> std::io::Result<()> { } #[allow(unreachable_code)] - Ok(()) + Ok::<(), String>(()) }); // Start the metrics server with actix_web only if metrics are not disabled @@ -1116,6 +275,27 @@ async fn main() -> std::io::Result<()> { } }; + // Setup periodic task runner for Lua scripts + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(60)); + loop { + interval.tick().await; + + // Trigger periodic event + let ctx = EventContext { + event_type: EventType::Periodic, + ip: None, + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: None, + }; + script_manager_for_periodic.trigger_event(&ctx); + } + }); + // Run both servers concurrently if metrics server is enabled if let Some(metrics_server) = metrics_server { tokio::select! { @@ -1123,11 +303,11 @@ async fn main() -> std::io::Result<()> { Ok(Ok(())) => Ok(()), Ok(Err(e)) => { log::error!("Tarpit server error: {e}"); - Err(std::io::Error::other(e)) + Err(std::io::Error::new(std::io::ErrorKind::Other, e)) }, Err(e) => { log::error!("Tarpit server task error: {e}"); - Err(std::io::Error::other(e.to_string())) + Err(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) }, }, result = metrics_server => { @@ -1143,197 +323,15 @@ async fn main() -> std::io::Result<()> { Ok(Ok(())) => Ok(()), Ok(Err(e)) => { log::error!("Tarpit server error: {e}"); - Err(std::io::Error::other(e)) + Err(std::io::Error::new(std::io::ErrorKind::Other, e)) } Err(e) => { log::error!("Tarpit server task error: {e}"); - Err(std::io::Error::other( + Err(std::io::Error::new( + std::io::ErrorKind::Other, e.to_string(), )) } } } } - -#[cfg(test)] -mod tests { - use super::*; - use std::net::{IpAddr, Ipv4Addr}; - use tokio::sync::RwLock; - - #[test] - fn test_config_from_args() { - let args = Args { - listen_addr: "127.0.0.1:8080".to_string(), - metrics_addr: "127.0.0.1:9000".to_string(), - disable_metrics: true, - backend_addr: "127.0.0.1:8081".to_string(), - min_delay: 500, - max_delay: 10000, - max_tarpit_time: 300, - block_threshold: 5, - base_dir: Some(PathBuf::from("/tmp/eris")), - config_file: None, - log_level: "debug".to_string(), - }; - - let config = Config::from_args(&args); - assert_eq!(config.listen_addr, "127.0.0.1:8080"); - assert_eq!(config.metrics_addr, "127.0.0.1:9000"); - assert!(config.disable_metrics); - assert_eq!(config.backend_addr, "127.0.0.1:8081"); - assert_eq!(config.min_delay, 500); - assert_eq!(config.max_delay, 10000); - assert_eq!(config.max_tarpit_time, 300); - assert_eq!(config.block_threshold, 5); - assert_eq!(config.markov_corpora_dir, "/tmp/eris/data/corpora"); - assert_eq!(config.lua_scripts_dir, "/tmp/eris/data/scripts"); - assert_eq!(config.data_dir, "/tmp/eris/data"); - assert_eq!(config.config_dir, "/tmp/eris/conf"); - assert_eq!(config.cache_dir, "/tmp/eris/cache"); - } - - #[tokio::test] - async fn test_should_tarpit() { - let config = Config::default(); - - // Test trap patterns - assert!( - should_tarpit( - "/vendor/phpunit/whatever", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); - assert!( - should_tarpit( - "/wp-admin/login.php", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); - assert!(should_tarpit("/.env", &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), &config).await); - - // Test whitelist networks - assert!( - !should_tarpit( - "/wp-admin/login.php", - &IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - &config - ) - .await - ); - assert!( - !should_tarpit( - "/vendor/phpunit/whatever", - &IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), - &config - ) - .await - ); - - // Test legitimate paths - assert!( - !should_tarpit( - "/index.html", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); - assert!( - !should_tarpit( - "/images/logo.png", - &IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), - &config - ) - .await - ); - } - - #[test] - fn test_script_manager_default_script() { - let script_manager = ScriptManager::new("/nonexistent_directory"); - assert!(script_manager.scripts_loaded); - assert!( - script_manager - .script_content - .contains("generate_honeytoken") - ); - assert!(script_manager.script_content.contains("enhance_response")); - } - - #[tokio::test] - async fn test_bot_state() { - let state = BotState::new("/tmp/eris_test", "/tmp/eris_test_cache"); - let ip1 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); - let ip2 = IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8)); - - let state = Arc::new(RwLock::new(state)); - - // Test hit counter - { - let mut state = state.write().await; - *state.hits.entry(ip1).or_insert(0) += 1; - *state.hits.entry(ip1).or_insert(0) += 1; - *state.hits.entry(ip2).or_insert(0) += 1; - - assert_eq!(*state.hits.get(&ip1).unwrap(), 2); - assert_eq!(*state.hits.get(&ip2).unwrap(), 1); - } - - // Test blocking - { - let mut state = state.write().await; - state.blocked.insert(ip1); - assert!(state.blocked.contains(&ip1)); - assert!(!state.blocked.contains(&ip2)); - } - - // Test active connections - { - let mut state = state.write().await; - state.active_connections.insert(ip1); - state.active_connections.insert(ip2); - assert_eq!(state.active_connections.len(), 2); - - state.active_connections.remove(&ip1); - assert_eq!(state.active_connections.len(), 1); - assert!(!state.active_connections.contains(&ip1)); - assert!(state.active_connections.contains(&ip2)); - } - } - - #[tokio::test] - async fn test_generate_deceptive_response() { - // Create a simple markov generator for testing - let markov = MarkovGenerator::new("/nonexistent/path"); - let script_manager = ScriptManager::new("/nonexistent/path"); - - // Test different path types - let resp1 = generate_deceptive_response( - "/vendor/phpunit/exec", - "TestBot/1.0", - &markov, - &script_manager, - ) - .await; - assert!(resp1.contains("HTTP/1.1 200 OK")); - assert!(resp1.contains("X-Powered-By: PHP")); - - let resp2 = - generate_deceptive_response("/wp-admin/", "TestBot/1.0", &markov, &script_manager) - .await; - assert!(resp2.contains("HTTP/1.1 200 OK")); - - let resp3 = - generate_deceptive_response("/api/users", "TestBot/1.0", &markov, &script_manager) - .await; - assert!(resp3.contains("HTTP/1.1 200 OK")); - - // Verify tracking token is included - assert!(resp1.contains("BOT_TestBot")); - } -} diff --git a/src/markov.rs b/src/markov.rs index 2da848a..05960cb 100644 --- a/src/markov.rs +++ b/src/markov.rs @@ -90,7 +90,6 @@ pub struct MarkovGenerator { } impl MarkovGenerator { - #[must_use] pub fn new(corpus_dir: &str) -> Self { let mut chains = HashMap::new(); @@ -102,22 +101,28 @@ impl MarkovGenerator { // Load corpus files if they exist let path = Path::new(corpus_dir); - if path.exists() && path.is_dir() - && let Ok(entries) = fs::read_dir(path) { - for entry in entries.flatten() { - let file_path = entry.path(); - if let Some(file_name) = file_path.file_stem() - && let Some(file_name_str) = file_name.to_str() - && types.contains(&file_name_str) - && let Ok(content) = fs::read_to_string(&file_path) { - let mut chain = Chain::new(DEFAULT_ORDER); - for line in content.lines() { - chain.add(line); + if path.exists() && path.is_dir() { + if let Ok(entries) = fs::read_dir(path) { + entries.for_each(|entry| { + if let Ok(entry) = entry { + let file_path = entry.path(); + if let Some(file_name) = file_path.file_stem() { + if let Some(file_name_str) = file_name.to_str() { + if types.contains(&file_name_str) { + if let Ok(content) = fs::read_to_string(&file_path) { + let mut chain = Chain::new(DEFAULT_ORDER); + for line in content.lines() { + chain.add(line); + } + chains.insert(file_name_str.to_string(), chain); } - chains.insert(file_name_str.to_string(), chain); } - } + } + } + } + }); } + } // If corpus files didn't exist, initialize with some default content if chains["php_exploit"].start_states.is_empty() { diff --git a/src/metrics.rs b/src/metrics.rs index 48bceb9..44c3fb4 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -24,6 +24,11 @@ lazy_static! { register_counter_vec!("eris_path_hits_total", "Hits by path", &["path"]).unwrap(); pub static ref UA_HITS: CounterVec = register_counter_vec!("eris_ua_hits_total", "Hits by user agent", &["user_agent"]).unwrap(); + pub static ref RATE_LIMITED_CONNECTIONS: Counter = register_counter!( + "eris_rate_limited_total", + "Number of connections rejected due to rate limiting" + ) + .unwrap(); } // Prometheus metrics endpoint @@ -76,6 +81,7 @@ mod tests { UA_HITS.with_label_values(&["TestBot/1.0"]).inc(); BLOCKED_IPS.set(5.0); ACTIVE_CONNECTIONS.set(3.0); + RATE_LIMITED_CONNECTIONS.inc(); // Create test app let app = @@ -96,6 +102,7 @@ mod tests { assert!(body_str.contains("eris_ua_hits_total")); assert!(body_str.contains("eris_blocked_ips")); assert!(body_str.contains("eris_active_connections")); + assert!(body_str.contains("eris_rate_limited_total")); } #[actix_web::test] diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..0fd1bfa --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1,504 @@ +use std::collections::HashMap; +use std::net::IpAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::process::Command; +use tokio::sync::RwLock; +use tokio::time::sleep; + +use crate::config::Config; +use crate::lua::{EventContext, EventType, ScriptManager}; +use crate::markov::MarkovGenerator; +use crate::metrics::{ + ACTIVE_CONNECTIONS, BLOCKED_IPS, HITS_COUNTER, PATH_HITS, RATE_LIMITED_CONNECTIONS, UA_HITS, +}; +use crate::state::BotState; +use crate::utils::{ + choose_response_type, extract_all_headers, extract_header_value, extract_path_from_request, + find_header_end, generate_session_id, get_timestamp, +}; + +mod rate_limiter; +use rate_limiter::RateLimiter; + +// Global rate limiter instance. +// Default is 30 connections per IP in a 60 second window +// XXX: This might add overhead of the proxy, e.g. NGINX already implements +// rate limiting. Though I don't think we have a way of knowing if the middleman +// we are handing the connections to (from the same middleman in some cases) has +// rate limiting. +lazy_static::lazy_static! { + static ref RATE_LIMITER: RateLimiter = RateLimiter::new(60, 30); +} + +// Main connection handler. +// Decides whether to tarpit or proxy +pub async fn handle_connection( + mut stream: TcpStream, + config: Arc, + state: Arc>, + markov_generator: Arc, + script_manager: Arc, +) { + // Get peer information + let peer_addr: IpAddr = match stream.peer_addr() { + Ok(addr) => addr.ip(), + Err(e) => { + log::debug!("Failed to get peer address: {e}"); + return; + } + }; + + // Check for blocked IPs to avoid any processing + if state.read().await.blocked.contains(&peer_addr) { + log::debug!("Rejected connection from blocked IP: {peer_addr}"); + let _ = stream.shutdown().await; + return; + } + + // Apply rate limiting before any further processing + if config.rate_limit_enabled && !RATE_LIMITER.check_rate_limit(peer_addr).await { + log::info!("Rate limited connection from {peer_addr}"); + RATE_LIMITED_CONNECTIONS.inc(); + + // Optionally, add the IP to a temporary block list + // if it's constantly hitting the rate limit + let connection_count = RATE_LIMITER.get_connection_count(&peer_addr); + if connection_count > config.rate_limit_block_threshold { + log::warn!( + "IP {peer_addr} exceeding rate limit with {connection_count} connection attempts, considering for blocking" + ); + + // Trigger a blocked event for Lua scripts + let rate_limit_ctx = EventContext { + event_type: EventType::BlockIP, + ip: Some(peer_addr.to_string()), + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: None, + }; + script_manager.trigger_event(&rate_limit_ctx); + } + + // Either send a slow response or just close connection + if config.rate_limit_slow_response { + // Send a simple 429 Too Many Requests respons. If the bots actually respected + // HTTP error codes, the internet would be a mildly better place. + let response = "HTTP/1.1 429 Too Many Requests\r\n\ + Content-Type: text/plain\r\n\ + Retry-After: 60\r\n\ + Connection: close\r\n\ + \r\n\ + Rate limit exceeded. Please try again later."; + + let _ = stream.write_all(response.as_bytes()).await; + let _ = stream.flush().await; + } + + let _ = stream.shutdown().await; + return; + } + + // Check if Lua scripts allow this connection + if !script_manager.on_connection(&peer_addr.to_string()) { + log::debug!("Connection rejected by Lua script: {peer_addr}"); + let _ = stream.shutdown().await; + return; + } + + // Pre-check for whitelisted IPs to bypass heavy processing + let mut whitelisted = false; + for network_str in &config.whitelist_networks { + if let Ok(network) = network_str.parse::() { + if network.contains(peer_addr) { + whitelisted = true; + break; + } + } + } + + // Read buffer + let mut buffer = vec![0; 8192]; + let mut request_data = Vec::with_capacity(8192); + let mut header_end_pos = 0; + + // Read with timeout to prevent hanging resource load ops. + let read_fut = async { + loop { + match stream.read(&mut buffer).await { + Ok(0) => break, + Ok(n) => { + let new_data = &buffer[..n]; + request_data.extend_from_slice(new_data); + + // Look for end of headers + if header_end_pos == 0 { + if let Some(pos) = find_header_end(&request_data) { + header_end_pos = pos; + break; + } + } + + // Avoid excessive buffering + if request_data.len() > 32768 { + break; + } + } + Err(e) => { + log::debug!("Error reading from stream: {e}"); + break; + } + } + } + }; + + let timeout_fut = sleep(Duration::from_secs(3)); + + tokio::select! { + () = read_fut => {}, + () = timeout_fut => { + log::debug!("Connection timeout from: {peer_addr}"); + let _ = stream.shutdown().await; + return; + } + } + + // Fast path for whitelisted IPs. Skip full parsing and speed up "approved" + // connections automatically. + if whitelisted { + log::debug!("Whitelisted IP {peer_addr} - using fast proxy path"); + proxy_fast_path(stream, request_data, &config.backend_addr).await; + return; + } + + // Parse minimally to extract the path + let path = if let Some(p) = extract_path_from_request(&request_data) { + p + } else { + log::debug!("Invalid request from {peer_addr}"); + let _ = stream.shutdown().await; + return; + }; + + // Extract request headers for Lua scripts + let headers = extract_all_headers(&request_data); + + // Extract user agent for logging and decision making + let user_agent = + extract_header_value(&request_data, "user-agent").unwrap_or_else(|| "unknown".to_string()); + + // Trigger request event for Lua scripts + let request_ctx = EventContext { + event_type: EventType::Request, + ip: Some(peer_addr.to_string()), + path: Some(path.to_string()), + user_agent: Some(user_agent.clone()), + request_headers: Some(headers.clone()), + content: None, + timestamp: get_timestamp(), + session_id: Some(generate_session_id(&peer_addr.to_string(), &user_agent)), + }; + script_manager.trigger_event(&request_ctx); + + // Check if this request matches our tarpit patterns + let should_tarpit = crate::config::should_tarpit(path, &peer_addr, &config); + + if should_tarpit { + log::info!("Tarpit triggered: {path} from {peer_addr} (UA: {user_agent})"); + + // Update metrics + HITS_COUNTER.inc(); + PATH_HITS.with_label_values(&[path]).inc(); + UA_HITS.with_label_values(&[&user_agent]).inc(); + + // Update state and check for blocking threshold + { + let mut state = state.write().await; + state.active_connections.insert(peer_addr); + ACTIVE_CONNECTIONS.set(state.active_connections.len() as f64); + + *state.hits.entry(peer_addr).or_insert(0) += 1; + let hit_count = state.hits[&peer_addr]; + + // Use Lua to decide whether to block this IP + let should_block = script_manager.should_block_ip(&peer_addr.to_string(), hit_count); + + // Block IPs that hit tarpits too many times + if should_block && !state.blocked.contains(&peer_addr) { + log::info!("Blocking IP {peer_addr} after {hit_count} hits"); + state.blocked.insert(peer_addr); + BLOCKED_IPS.set(state.blocked.len() as f64); + state.save_to_disk(); + + // Do firewall blocking in background + let peer_addr_str = peer_addr.to_string(); + tokio::spawn(async move { + log::debug!("Adding IP {peer_addr_str} to firewall blacklist"); + match Command::new("nft") + .args([ + "add", + "element", + "inet", + "filter", + "eris_blacklist", + "{", + &peer_addr_str, + "}", + ]) + .output() + .await + { + Ok(output) => { + if !output.status.success() { + log::warn!( + "Failed to add IP {} to firewall: {}", + peer_addr_str, + String::from_utf8_lossy(&output.stderr) + ); + } + } + Err(e) => { + log::warn!("Failed to execute nft command: {e}"); + } + } + }); + } + } + + // Generate a deceptive response using Markov chains and Lua + let response = generate_deceptive_response( + path, + &user_agent, + &peer_addr, + &headers, + &markov_generator, + &script_manager, + ) + .await; + + // Generate a session ID for tracking this tarpit session + let session_id = generate_session_id(&peer_addr.to_string(), &user_agent); + + // Send the response with the tarpit delay strategy + { + let mut stream = stream; + let peer_addr = peer_addr; + let state = state.clone(); + let min_delay = config.min_delay; + let max_delay = config.max_delay; + let max_tarpit_time = config.max_tarpit_time; + let script_manager = script_manager.clone(); + async move { + let start_time = Instant::now(); + let mut chars = response.chars().collect::>(); + for i in (0..chars.len()).rev() { + if i > 0 && rand::random::() < 0.1 { + chars.swap(i, i - 1); + } + } + log::debug!( + "Starting tarpit for {} with {} chars, min_delay={}ms, max_delay={}ms", + peer_addr, + chars.len(), + min_delay, + max_delay + ); + let mut position = 0; + let mut chunks_sent = 0; + let mut total_delay = 0; + while position < chars.len() { + // Check if we've exceeded maximum tarpit time + let elapsed_secs = start_time.elapsed().as_secs(); + if elapsed_secs > max_tarpit_time { + log::info!( + "Tarpit maximum time ({max_tarpit_time} sec) reached for {peer_addr}" + ); + break; + } + + // Decide how many chars to send in this chunk (usually 1, sometimes more) + let chunk_size = if rand::random::() < 0.9 { + 1 + } else { + (rand::random::() * 3.0).floor() as usize + 1 + }; + + let end = (position + chunk_size).min(chars.len()); + let chunk: String = chars[position..end].iter().collect(); + + // Process chunk through Lua before sending + let processed_chunk = + script_manager.process_chunk(&chunk, &peer_addr.to_string(), &session_id); + + // Try to write processed chunk + if stream.write_all(processed_chunk.as_bytes()).await.is_err() { + log::debug!("Connection closed by client during tarpit: {peer_addr}"); + break; + } + + if stream.flush().await.is_err() { + log::debug!("Failed to flush stream during tarpit: {peer_addr}"); + break; + } + + position = end; + chunks_sent += 1; + + // Apply random delay between min and max configured values + let delay_ms = + (rand::random::() * (max_delay - min_delay) as f32) as u64 + min_delay; + total_delay += delay_ms; + sleep(Duration::from_millis(delay_ms)).await; + } + log::debug!( + "Tarpit stats for {}: sent {} chunks, {}% of data, total delay {}ms over {}s", + peer_addr, + chunks_sent, + position * 100 / chars.len(), + total_delay, + start_time.elapsed().as_secs() + ); + let disconnection_ctx = EventContext { + event_type: EventType::Disconnection, + ip: Some(peer_addr.to_string()), + path: None, + user_agent: None, + request_headers: None, + content: None, + timestamp: get_timestamp(), + session_id: Some(session_id), + }; + script_manager.trigger_event(&disconnection_ctx); + if let Ok(mut state) = state.try_write() { + state.active_connections.remove(&peer_addr); + ACTIVE_CONNECTIONS.set(state.active_connections.len() as f64); + } + let _ = stream.shutdown().await; + } + } + .await; + } else { + log::debug!("Proxying request: {path} from {peer_addr}"); + proxy_fast_path(stream, request_data, &config.backend_addr).await; + } +} + +// Forward a legitimate request to the real backend server +pub async fn proxy_fast_path( + mut client_stream: TcpStream, + request_data: Vec, + backend_addr: &str, +) { + // Connect to backend server + let server_stream = match TcpStream::connect(backend_addr).await { + Ok(stream) => stream, + Err(e) => { + log::warn!("Failed to connect to backend {backend_addr}: {e}"); + let _ = client_stream.shutdown().await; + return; + } + }; + + // Set TCP_NODELAY for both streams before splitting them + if let Err(e) = client_stream.set_nodelay(true) { + log::debug!("Failed to set TCP_NODELAY on client stream: {e}"); + } + + let mut server_stream = server_stream; + if let Err(e) = server_stream.set_nodelay(true) { + log::debug!("Failed to set TCP_NODELAY on server stream: {e}"); + } + + // Forward the original request bytes directly without parsing + if server_stream.write_all(&request_data).await.is_err() { + log::debug!("Failed to write request to backend server"); + let _ = client_stream.shutdown().await; + return; + } + + // Now split the streams for concurrent reading/writing + let (mut client_read, mut client_write) = client_stream.split(); + let (mut server_read, mut server_write) = server_stream.split(); + + // 32KB buffer + let buf_size = 32768; + + // Client -> Server + let client_to_server = async { + let mut buf = vec![0; buf_size]; + let mut bytes_forwarded = 0; + + loop { + match client_read.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + bytes_forwarded += n; + if server_write.write_all(&buf[..n]).await.is_err() { + break; + } + } + Err(_) => break, + } + } + + // Ensure everything is sent + let _ = server_write.flush().await; + log::debug!("Client -> Server: forwarded {bytes_forwarded} bytes"); + }; + + // Server -> Client + let server_to_client = async { + let mut buf = vec![0; buf_size]; + let mut bytes_forwarded = 0; + + loop { + match server_read.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + bytes_forwarded += n; + if client_write.write_all(&buf[..n]).await.is_err() { + break; + } + } + Err(_) => break, + } + } + + // Ensure everything is sent + let _ = client_write.flush().await; + log::debug!("Server -> Client: forwarded {bytes_forwarded} bytes"); + }; + + // Run both directions concurrently + tokio::join!(client_to_server, server_to_client); + log::debug!("Fast proxy connection completed"); +} + +// Generate a deceptive HTTP response that appears legitimate +pub async fn generate_deceptive_response( + path: &str, + user_agent: &str, + peer_addr: &IpAddr, + headers: &HashMap, + markov: &MarkovGenerator, + script_manager: &ScriptManager, +) -> String { + // Generate base response using Markov chain text generator + let response_type = choose_response_type(path); + let markov_text = markov.generate(response_type, 30); + + // Use Lua scripts to enhance with honeytokens and other deceptive content + script_manager.generate_response( + path, + user_agent, + &peer_addr.to_string(), + headers, + &markov_text, + ) +} diff --git a/src/network/rate_limiter.rs b/src/network/rate_limiter.rs new file mode 100644 index 0000000..6150793 --- /dev/null +++ b/src/network/rate_limiter.rs @@ -0,0 +1,85 @@ +use std::collections::HashMap; +use std::net::IpAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::Mutex; + +pub struct RateLimiter { + connections: Arc>>>, + window_seconds: u64, + max_connections: usize, + cleanup_interval: Duration, + last_cleanup: Instant, +} + +impl RateLimiter { + pub fn new(window_seconds: u64, max_connections: usize) -> Self { + Self { + connections: Arc::new(Mutex::new(HashMap::new())), + window_seconds, + max_connections, + cleanup_interval: Duration::from_secs(60), + last_cleanup: Instant::now(), + } + } + + pub async fn check_rate_limit(&self, ip: IpAddr) -> bool { + let now = Instant::now(); + let window = Duration::from_secs(self.window_seconds); + + let mut connections = self.connections.lock().await; + + // Periodically clean up old entries across all IPs + if now.duration_since(self.last_cleanup) > self.cleanup_interval { + self.cleanup_old_entries(&mut connections, now, window); + } + + // Clean up old entries for this specific IP + if let Some(times) = connections.get_mut(&ip) { + times.retain(|time| now.duration_since(*time) < window); + + // Check if rate limit exceeded + if times.len() >= self.max_connections { + log::debug!("Rate limit exceeded for IP: {}", ip); + return false; + } + + // Add new connection time + times.push(now); + } else { + connections.insert(ip, vec![now]); + } + + true + } + + fn cleanup_old_entries( + &self, + connections: &mut HashMap>, + now: Instant, + window: Duration, + ) { + let mut empty_keys = Vec::new(); + + for (ip, times) in connections.iter_mut() { + times.retain(|time| now.duration_since(*time) < window); + if times.is_empty() { + empty_keys.push(*ip); + } + } + + // Remove empty entries + for ip in empty_keys { + connections.remove(&ip); + } + } + + pub fn get_connection_count(&self, ip: &IpAddr) -> usize { + if let Ok(connections) = self.connections.try_lock() { + if let Some(times) = connections.get(ip) { + return times.len(); + } + } + 0 + } +} diff --git a/src/lib.rs b/src/state.rs similarity index 53% rename from src/lib.rs rename to src/state.rs index 7bbc595..99e5369 100644 --- a/src/lib.rs +++ b/src/state.rs @@ -1,20 +1,11 @@ -//! Eris - Sophisticated HTTP tarpit and honeypot use std::collections::{HashMap, HashSet}; +use std::fs; +use std::io::Write; use std::net::IpAddr; -pub mod error; -pub mod markov; -pub mod metrics; +use crate::metrics::BLOCKED_IPS; -// Re-export commonly used types -pub use error::{ErisError, Result}; -pub use markov::MarkovGenerator; -pub use metrics::{ - ACTIVE_CONNECTIONS, BLOCKED_IPS, HITS_COUNTER, PATH_HITS, UA_HITS, metrics_handler, - status_handler, -}; - -/// State of bots/IPs hitting the honeypot +// State of bots/IPs hitting the honeypot #[derive(Clone, Debug)] pub struct BotState { pub hits: HashMap, @@ -25,7 +16,6 @@ pub struct BotState { } impl BotState { - #[must_use] pub fn new(data_dir: &str, cache_dir: &str) -> Self { Self { hits: HashMap::new(), @@ -36,16 +26,15 @@ impl BotState { } } - /// Load previous state from disk - #[must_use] + // Load previous state from disk pub fn load_from_disk(data_dir: &str, cache_dir: &str) -> Self { let mut state = Self::new(data_dir, cache_dir); let blocked_ips_file = format!("{data_dir}/blocked_ips.txt"); - if let Ok(content) = std::fs::read_to_string(&blocked_ips_file) { + if let Ok(content) = fs::read_to_string(&blocked_ips_file) { let mut loaded = 0; for line in content.lines() { - if let Ok(ip) = line.parse::() { + if let Ok(ip) = line.parse::() { state.blocked.insert(ip); loaded += 1; } @@ -57,37 +46,36 @@ impl BotState { // Check for temporary hit counter cache let hit_cache_file = format!("{cache_dir}/hit_counters.json"); - if let Ok(content) = std::fs::read_to_string(&hit_cache_file) - && let Ok(hit_map) = - serde_json::from_str::>(&content) - { + if let Ok(content) = fs::read_to_string(&hit_cache_file) { + if let Ok(hit_map) = serde_json::from_str::>(&content) { for (ip_str, count) in hit_map { - if let Ok(ip) = ip_str.parse::() { + if let Ok(ip) = ip_str.parse::() { state.hits.insert(ip, count); } } log::info!("Loaded hit counters for {} IPs", state.hits.len()); } + } BLOCKED_IPS.set(state.blocked.len() as f64); state } - /// Persist state to disk for later reloading + // Persist state to disk for later reloading pub fn save_to_disk(&self) { // Save blocked IPs - if let Err(e) = std::fs::create_dir_all(&self.data_dir) { + if let Err(e) = fs::create_dir_all(&self.data_dir) { log::error!("Failed to create data directory: {e}"); return; } let blocked_ips_file = format!("{}/blocked_ips.txt", self.data_dir); - match std::fs::File::create(&blocked_ips_file) { + match fs::File::create(&blocked_ips_file) { Ok(mut file) => { let mut count = 0; for ip in &self.blocked { - if std::io::Write::write_fmt(&mut file, format_args!("{ip}\n")).is_ok() { + if writeln!(file, "{ip}").is_ok() { count += 1; } } @@ -99,18 +87,19 @@ impl BotState { } // Save hit counters to cache - if let Err(e) = std::fs::create_dir_all(&self.cache_dir) { + if let Err(e) = fs::create_dir_all(&self.cache_dir) { log::error!("Failed to create cache directory: {e}"); return; } let hit_cache_file = format!("{}/hit_counters.json", self.cache_dir); - let mut hit_map = std::collections::HashMap::new(); - for (ip, count) in &self.hits { - hit_map.insert(ip.to_string(), *count); - } + let hit_map: HashMap = self + .hits + .iter() + .map(|(ip, count)| (ip.to_string(), *count)) + .collect(); - match std::fs::File::create(&hit_cache_file) { + match fs::File::create(&hit_cache_file) { Ok(file) => { if let Err(e) = serde_json::to_writer(file, &hit_map) { log::error!("Failed to write hit counters to cache: {e}"); @@ -128,14 +117,50 @@ impl BotState { #[cfg(test)] mod tests { use super::*; + use std::net::{IpAddr, Ipv4Addr}; + use std::sync::Arc; + use tokio::sync::RwLock; - #[test] - fn test_library_imports() { - // Test that we can import and use the main types - let _err = ErisError::config("test"); - let _result: Result<()> = Ok(()); + #[tokio::test] + async fn test_bot_state() { + let state = BotState::new("/tmp/eris_test", "/tmp/eris_test_cache"); + let ip1 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)); + let ip2 = IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8)); - // Test markov generator creation - let _markov = MarkovGenerator::new("./test_corpora"); + let state = Arc::new(RwLock::new(state)); + + // Test hit counter + { + let mut state = state.write().await; + *state.hits.entry(ip1).or_insert(0) += 1; + *state.hits.entry(ip1).or_insert(0) += 1; + *state.hits.entry(ip2).or_insert(0) += 1; + + assert_eq!(*state.hits.get(&ip1).unwrap(), 2); + assert_eq!(*state.hits.get(&ip2).unwrap(), 1); + } + + // Test blocking + { + let mut state = state.write().await; + state.blocked.insert(ip1); + assert!(state.blocked.contains(&ip1)); + assert!(!state.blocked.contains(&ip2)); + drop(state); + } + + // Test active connections + { + let mut state = state.write().await; + state.active_connections.insert(ip1); + state.active_connections.insert(ip2); + assert_eq!(state.active_connections.len(), 2); + + state.active_connections.remove(&ip1); + assert_eq!(state.active_connections.len(), 1); + assert!(!state.active_connections.contains(&ip1)); + assert!(state.active_connections.contains(&ip2)); + drop(state); + } } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c925565 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,168 @@ +use std::collections::HashMap; +use std::hash::Hasher; + +// Find end of HTTP headers +pub fn find_header_end(data: &[u8]) -> Option { + data.windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|pos| pos + 4) +} + +// Extract path from raw request data +pub fn extract_path_from_request(data: &[u8]) -> Option<&str> { + // Get first line from request + let first_line = data + .split(|&b| b == b'\r' || b == b'\n') + .next() + .filter(|line| !line.is_empty())?; + + // Split by spaces and ensure we have at least 3 parts (METHOD PATH VERSION) + let parts: Vec<&[u8]> = first_line.split(|&b| b == b' ').collect(); + if parts.len() < 3 || !parts[2].starts_with(b"HTTP/") { + return None; + } + + // Return the path (second element) + std::str::from_utf8(parts[1]).ok() +} + +// Extract header value from raw request data +pub fn extract_header_value(data: &[u8], header_name: &str) -> Option { + let data_str = std::str::from_utf8(data).ok()?; + let header_prefix = format!("{header_name}: ").to_lowercase(); + + for line in data_str.lines() { + let line_lower = line.to_lowercase(); + if line_lower.starts_with(&header_prefix) { + return Some(line[header_prefix.len()..].trim().to_string()); + } + } + None +} + +// Extract all headers from request data +pub fn extract_all_headers(data: &[u8]) -> HashMap { + let mut headers = HashMap::new(); + + if let Ok(data_str) = std::str::from_utf8(data) { + let mut lines = data_str.lines(); + + // Skip the request line + let _ = lines.next(); + + // Parse headers until empty line + for line in lines { + if line.is_empty() { + break; + } + + if let Some(colon_pos) = line.find(':') { + let key = line[..colon_pos].trim().to_lowercase(); + let value = line[colon_pos + 1..].trim().to_string(); + headers.insert(key, value); + } + } + } + + headers +} + +// Determine response type based on request path +pub fn choose_response_type(path: &str) -> &'static str { + if path.contains("phpunit") || path.contains("eval") { + "php_exploit" + } else if path.contains("wp-") { + "wordpress" + } else if path.contains("api") { + "api" + } else { + "generic" + } +} + +// Get current timestamp in seconds +pub fn get_timestamp() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +// Create a unique session ID for tracking a connection +pub fn generate_session_id(ip: &str, user_agent: &str) -> String { + let timestamp = get_timestamp(); + let random = rand::random::(); + + // XXX: Is this fast enough for our case? I don't think hashing is a huge + // bottleneck, but it's worth revisiting in the future to see if there is + // an objectively faster algorithm that we can try. + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + std::hash::Hash::hash(&format!("{ip}_{user_agent}_{timestamp}"), &mut hasher); + let hash = hasher.finish(); + + format!("SID_{hash:x}_{random:x}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_header_end() { + let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test\r\n\r\nBody content"; + assert_eq!(find_header_end(data), Some(55)); + + let incomplete = b"GET / HTTP/1.1\r\nHost: example.com\r\n"; + assert_eq!(find_header_end(incomplete), None); + } + + #[test] + fn test_extract_path_from_request() { + let data = b"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"; + assert_eq!(extract_path_from_request(data), Some("/index.html")); + + let bad_data = b"INVALID DATA"; + assert_eq!(extract_path_from_request(bad_data), None); + } + + #[test] + fn test_extract_header_value() { + let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: TestBot/1.0\r\n\r\n"; + assert_eq!( + extract_header_value(data, "user-agent"), + Some("TestBot/1.0".to_string()) + ); + assert_eq!( + extract_header_value(data, "Host"), + Some("example.com".to_string()) + ); + assert_eq!(extract_header_value(data, "nonexistent"), None); + } + + #[test] + fn test_extract_all_headers() { + let data = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: TestBot/1.0\r\nAccept: */*\r\n\r\n"; + let headers = extract_all_headers(data); + + assert_eq!(headers.len(), 3); + assert_eq!(headers.get("host").unwrap(), "example.com"); + assert_eq!(headers.get("user-agent").unwrap(), "TestBot/1.0"); + assert_eq!(headers.get("accept").unwrap(), "*/*"); + } + + #[test] + fn test_choose_response_type() { + assert_eq!( + choose_response_type("/vendor/phpunit/whatever"), + "php_exploit" + ); + assert_eq!( + choose_response_type("/path/to/eval-stdin.php"), + "php_exploit" + ); + assert_eq!(choose_response_type("/wp-admin/login.php"), "wordpress"); + assert_eq!(choose_response_type("/wp-login.php"), "wordpress"); + assert_eq!(choose_response_type("/api/v1/users"), "api"); + assert_eq!(choose_response_type("/index.html"), "generic"); + } +}