diff --git a/Cargo.lock b/Cargo.lock index 32b213a..0838e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ "futures-core", "tokio", @@ -266,12 +266,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[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" @@ -518,9 +512,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -597,10 +591,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.34" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -620,23 +615,22 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] name = "clap" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -644,9 +638,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.46" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -656,9 +650,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -763,6 +757,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -811,9 +846,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -905,12 +940,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -976,6 +1011,12 @@ dependencies = [ "log", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "findshlibs" version = "0.10.2" @@ -1052,6 +1093,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1138,6 +1194,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1182,7 +1239,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", "wasm-bindgen", ] @@ -1296,7 +1353,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1573,9 +1630,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown", @@ -1633,9 +1690,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -1701,18 +1758,18 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -1750,9 +1807,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] @@ -2278,11 +2335,31 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" +checksum = "4c83367ba62b3f1dbd0f086ede4e5ebfb4713fb234dbbc5807772a31245ff46d" dependencies = [ "pem", "ring", @@ -2469,15 +2546,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2516,9 +2593,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" dependencies = [ "ring", "rustls-pki-types", @@ -2548,11 +2625,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -2576,9 +2653,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3133,15 +3210,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3176,12 +3253,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde", @@ -3191,15 +3267,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3365,7 +3441,7 @@ checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "torrust-actix" -version = "4.0.13" +version = "4.0.14" dependencies = [ "actix-cors", "actix-web", @@ -3375,12 +3451,16 @@ dependencies = [ "byteorder", "chrono", "clap", + "crossbeam", "fern", + "futures", "futures-util", "hex", + "lazy_static", "log", "parking_lot", "percent-encoding", + "rayon", "rcgen", "regex", "rustls", @@ -3483,9 +3563,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "tracing-core", ] @@ -3525,9 +3605,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -3558,9 +3638,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00432f493971db5d8e47a65aeb3b02f8226b9b11f1450ff86bb772776ebadd70" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" dependencies = [ "base64", "der", @@ -3578,9 +3658,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b6cabebbecc4c45189ab06b52f956206cea7d8c8a20851c35a85cb169224cc" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64", "http 1.3.1", @@ -3662,9 +3742,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "js-sys", "serde", @@ -3722,9 +3802,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ "wit-bindgen", ] @@ -3737,21 +3826,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", @@ -3763,9 +3853,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -3776,9 +3866,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3786,9 +3876,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", @@ -3799,18 +3889,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -3881,11 +3971,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3902,7 +3992,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -3935,13 +4025,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3950,7 +4046,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3989,6 +4085,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4026,7 +4131,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4192,9 +4297,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "writeable" @@ -4237,18 +4342,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -4331,9 +4436,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" @@ -4367,9 +4472,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 40470d3..ac80364 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "torrust-actix" -version = "4.0.13" +version = "4.0.14" edition = "2024" license = "AGPL-3.0" authors = [ @@ -10,7 +10,7 @@ authors = [ description = "A rich, fast and efficient Bittorrent Tracker." [profile.release] -opt-level = 'z' +opt-level = 3 debug = false debug-assertions = false overflow-checks = false @@ -40,8 +40,8 @@ rcgen = "^0.14" regex = "^1.11" rustls = { version = "^0.23", default-features = false, features = ["std", "ring"] } rustls-pemfile = "^2.2" -sentry = { version = "^0.42", default-features = false, features = ["rustls", "backtrace", "contexts", "panic", "transport", "debug-images", "reqwest"] } -sentry-actix = "^0.42" +sentry = { version = "^0", default-features = false, features = ["rustls", "backtrace", "contexts", "panic", "transport", "debug-images", "reqwest"] } +sentry-actix = "^0" serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0", features = ["preserve_order"] } serde_millis = "^0.1" @@ -55,6 +55,10 @@ toml = "^0.9" tracing = "^0.1" utoipa = { version = "^5", features = ["actix_extras"] } utoipa-swagger-ui = { version = "^9", features = ["actix-web"] } +lazy_static = "^1.5" +crossbeam = "^0.8" +futures = "^0.3" +rayon = "^1.11" [target.'cfg(windows)'.build-dependencies] winres = "^0.1" diff --git a/README.md b/README.md index c308e08..9c80847 100644 --- a/README.md +++ b/README.md @@ -167,11 +167,20 @@ HTTP_0_TLS_CONNECTION_RATE UDP_0_ENABLED UDP_0_BIND_ADDRESS -UDP_0_THREADS +UDP_0_UDP_THREADS +UDP_0_UDP_WORKER_THREADS +UDP_0_UDP_RECEIVE_BUFFER_SIZE +UDP_0_UDP_SEND_BUFFER_SIZE +UDP_0_UDP_REUSE_ADDRESS ``` ### ChangeLog +#### v4.0.14 +* Code optimizations thanks to AI scanning +* Huge memory and CPU consumption improvement for UDP, using offloading +* Complete scan of code for multiple other performance improvements + #### v4.0.13 * Added further UDP improvement by adding customization, also added to the config: * Receive Buffer Size diff --git a/docker/Dockerfile b/docker/Dockerfile index f02d4b0..1acb7ce 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,14 +2,14 @@ FROM rust:alpine RUN apk add git musl-dev curl pkgconfig openssl-dev openssl-libs-static RUN git clone https://github.com/Power2All/torrust-actix.git /app/torrust-actix -RUN cd /app/torrust-actix && git checkout tags/v4.0.13 +RUN cd /app/torrust-actix && git checkout tags/v4.0.14 WORKDIR /app/torrust-actix RUN cd /app/torrust-actix RUN cargo build --release && rm -Rf target/release/.fingerprint target/release/build target/release/deps target/release/examples target/release/incremental COPY init.sh /app/torrust-actix/target/release/init.sh COPY healthcheck.py /app/torrust-actix/healthcheck RUN chmod +x /app/torrust-actix/target/release/init.sh -RUN chmod +x /app/torrust-actix/healthcheck.py +RUN chmod +x /app/torrust-actix/healthcheck EXPOSE 8080/tcp EXPOSE 6969/tcp EXPOSE 6969/udp diff --git a/src/api/api.rs b/src/api/api.rs index f24201b..3de1103 100644 --- a/src/api/api.rs +++ b/src/api/api.rs @@ -31,7 +31,6 @@ use crate::tracker::structs::torrent_tracker::TorrentTracker; #[tracing::instrument(level = "debug")] pub fn api_service_cors() -> Cors { - // This is not a duplicate, each framework has their own CORS configuration. Cors::default() .send_wildcard() .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"]) @@ -41,19 +40,15 @@ pub fn api_service_cors() -> Cors } #[tracing::instrument(level = "debug")] -pub fn api_service_routes(data: Arc) -> Box +pub fn api_service_routes(data: Arc) -> Box { Box::new(move |cfg: &mut ServiceConfig| { - cfg.app_data(Data::new(data.clone())); + cfg.app_data(Data::new(Arc::clone(&data))); cfg.default_service(web::route().to(api_service_not_found)); - cfg.service(web::resource("stats") - .route(web::get().to(api_service_stats_get)) - ); - cfg.service(web::resource("metrics") - .route(web::get().to(api_service_prom_get)) - ); - // Torrents API Routing + cfg.service(web::resource("stats").route(web::get().to(api_service_stats_get))); + cfg.service(web::resource("metrics").route(web::get().to(api_service_prom_get))); + cfg.service(web::resource("api/torrent/{info_hash}") .route(web::get().to(api_service_torrent_get)) .route(web::delete().to(api_service_torrent_delete)) @@ -65,7 +60,6 @@ pub fn api_service_routes(data: Arc) -> Box) -> Box) -> Box) -> Box) -> Box { data } - Err(data) => { - sentry::capture_error(&data); - panic!("[APIS] SSL key unreadable: {data}"); - } - }); - let certs_file = &mut BufReader::new(match File::open(api_server_object.ssl_cert.clone()) { - Ok(data) => { data } - Err(data) => { - sentry::capture_error(&data); - panic!("[APIS] SSL cert unreadable: {data}"); - } + let key_file = &mut BufReader::new(File::open(api_server_object.ssl_key.clone()).unwrap_or_else(|data| { + sentry::capture_error(&data); + panic!("[APIS] SSL key unreadable: {data}"); + })); + + let certs_file = &mut BufReader::new(File::open(api_server_object.ssl_cert.clone()).unwrap_or_else(|data| { + sentry::capture_error(&data); + panic!("[APIS] SSL cert unreadable: {data}"); + })); + + let tls_certs = rustls_pemfile::certs(certs_file).collect::, _>>().unwrap_or_else(|data| { + sentry::capture_error(&data); + panic!("[APIS] SSL cert couldn't be extracted: {data}"); }); - let tls_certs = match rustls_pemfile::certs(certs_file).collect::, _>>() { - Ok(data) => { data } - Err(data) => { - sentry::capture_error(&data); - panic!("[APIS] SSL cert couldn't be extracted: {data}"); - } - }; - let tls_key = match rustls_pemfile::pkcs8_private_keys(key_file).next().unwrap() { - Ok(data) => { data } - Err(data) => { - sentry::capture_error(&data); - panic!("[APIS] SSL key couldn't be extracted: {data}"); - } - }; + let tls_key = rustls_pemfile::pkcs8_private_keys(key_file).next().unwrap().unwrap_or_else(|data| { + sentry::capture_error(&data); + panic!("[APIS] SSL key couldn't be extracted: {data}"); + }); - let tls_config = match rustls::ServerConfig::builder().with_no_client_auth().with_single_cert(tls_certs, rustls::pki_types::PrivateKeyDer::Pkcs8(tls_key)) { - Ok(data) => { data } - Err(data) => { + let tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(tls_certs, rustls::pki_types::PrivateKeyDer::Pkcs8(tls_key)) + .unwrap_or_else(|data| { sentry::capture_error(&data); panic!("[APIS] SSL config couldn't be created: {data}"); - } - }; - - let server = match data.config.sentry_config.clone().enabled { - true => { - HttpServer::new(move || { App::new() - .wrap(api_service_cors()) - .wrap(sentry_actix::Sentry::new()) - .configure(api_service_routes(Arc::new(ApiServiceData { - torrent_tracker: data.clone(), - api_trackers_config: Arc::new(api_server_object.clone()) - })))}) - .keep_alive(Duration::from_secs(keep_alive)) - .client_request_timeout(Duration::from_secs(request_timeout)) - .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) - .workers(worker_threads) - .bind_rustls_0_23((addr.ip(), addr.port()), tls_config) - .unwrap() - .disable_signals() - .run() - } - false => { - HttpServer::new(move || { App::new() - .wrap(api_service_cors()) - .wrap(sentry_actix::Sentry::new()) - .configure(api_service_routes(Arc::new(ApiServiceData { - torrent_tracker: data.clone(), - api_trackers_config: Arc::new(api_server_object.clone()) - })))}) - .keep_alive(Duration::from_secs(keep_alive)) - .client_request_timeout(Duration::from_secs(request_timeout)) - .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) - .workers(worker_threads) - .bind_rustls_0_23((addr.ip(), addr.port()), tls_config) - .unwrap() - .disable_signals() - .run() - } - }; + }); + + let server = HttpServer::new(app_factory) + .keep_alive(Duration::from_secs(keep_alive)) + .client_request_timeout(Duration::from_secs(request_timeout)) + .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) + .workers(worker_threads) + .bind_rustls_0_23((addr.ip(), addr.port()), tls_config) + .unwrap() + .disable_signals() + .run(); return (server.handle(), server); } info!("[API] Starting server listener on {addr}"); - let server = match data.config.sentry_config.clone().enabled { - true => { - HttpServer::new(move || { App::new() - .wrap(api_service_cors()) - .wrap(sentry_actix::Sentry::new()) - .wrap(sentry_actix::Sentry::new()) - .configure(api_service_routes(Arc::new(ApiServiceData { - torrent_tracker: data.clone(), - api_trackers_config: Arc::new(api_server_object.clone()) - })))}) - .keep_alive(Duration::from_secs(keep_alive)) - .client_request_timeout(Duration::from_secs(request_timeout)) - .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) - .workers(worker_threads) - .bind((addr.ip(), addr.port())) - .unwrap() - .disable_signals() - .run() - } - false => { - HttpServer::new(move || { App::new() - .wrap(api_service_cors()) - .wrap(sentry_actix::Sentry::new()) - .configure(api_service_routes(Arc::new(ApiServiceData { - torrent_tracker: data.clone(), - api_trackers_config: Arc::new(api_server_object.clone()) - })))}) - .keep_alive(Duration::from_secs(keep_alive)) - .client_request_timeout(Duration::from_secs(request_timeout)) - .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) - .workers(worker_threads) - .bind((addr.ip(), addr.port())) - .unwrap() - .disable_signals() - .run() - } - }; + let server = HttpServer::new(app_factory) + .keep_alive(Duration::from_secs(keep_alive)) + .client_request_timeout(Duration::from_secs(request_timeout)) + .client_disconnect_timeout(Duration::from_secs(disconnect_timeout)) + .workers(worker_threads) + .bind((addr.ip(), addr.port())) + .unwrap() + .disable_signals() + .run(); (server.handle(), server) } @@ -270,68 +210,54 @@ pub async fn api_service( #[tracing::instrument(level = "debug")] pub async fn api_service_stats_log(ip: IpAddr, tracker: Arc) { - if ip.is_ipv4() { - tracker.update_stats(StatsEvent::Tcp4ConnectionsHandled, 1); + let event = if ip.is_ipv4() { + StatsEvent::Tcp4ConnectionsHandled } else { - tracker.update_stats(StatsEvent::Tcp6ConnectionsHandled, 1); - } + StatsEvent::Tcp6ConnectionsHandled + }; + tracker.update_stats(event, 1); } #[tracing::instrument(level = "debug")] pub async fn api_service_token(token: Option, config: Arc) -> Option { - match token { + let token_code = match token { + Some(token) => token, None => { - Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({ + return Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({ "status": "missing token" - }))) - } - Some(token_code) => { - if token_code != config.tracker_config.clone().api_key { - return Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({ - "status": "invalid token" - }))); - } - None + }))); } + }; + + if token_code != config.tracker_config.api_key { + return Some(HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({ + "status": "invalid token" + }))); } + + None } #[tracing::instrument(level = "debug")] pub async fn api_service_retrieve_remote_ip(request: &HttpRequest, data: Arc) -> Result { - let origin_ip = match request.peer_addr() { - None => { - return Err(()); - } - Some(ip) => { - ip.ip() - } - }; - match request.headers().get(data.real_ip.clone()) { - Some(header) => { - if header.to_str().is_ok() { - if let Ok(ip) = IpAddr::from_str(header.to_str().unwrap()) { - Ok(ip) - } else { - Err(()) - } - } else { - Err(()) - } - } - None => { - Ok(origin_ip) - } - } + let origin_ip = request.peer_addr().map(|addr| addr.ip()).ok_or(())?; + + request.headers() + .get(&data.real_ip) + .and_then(|header| header.to_str().ok()) + .and_then(|ip_str| IpAddr::from_str(ip_str).ok()) + .map(Ok) + .unwrap_or(Ok(origin_ip)) } #[tracing::instrument(level = "debug")] pub async fn api_validate_ip(request: &HttpRequest, data: Data>) -> Result { - match api_service_retrieve_remote_ip(request, data.api_trackers_config.clone()).await { + match api_service_retrieve_remote_ip(request, Arc::clone(&data.api_trackers_config)).await { Ok(ip) => { - api_service_stats_log(ip, data.torrent_tracker.clone()).await; + api_service_stats_log(ip, Arc::clone(&data.torrent_tracker)).await; Ok(ip) } Err(_) => { @@ -345,8 +271,9 @@ pub async fn api_validate_ip(request: &HttpRequest, data: Data>) -> HttpResponse { - // Validate client - if let Some(error_return) = api_validation(&request, &data).await { return error_return; } + if let Some(error_return) = api_validation(&request, &data).await { + return error_return; + } HttpResponse::NotFound().content_type(ContentType::json()).json(json!({ "status": "not found" @@ -356,26 +283,30 @@ pub async fn api_service_not_found(request: HttpRequest, data: Data, stats_ipv4: StatsEvent, stat_ipv6: StatsEvent, count: i64) { - match ip { - IpAddr::V4(_) => { - let data_clone = data.clone(); - data_clone.update_stats(stats_ipv4, count); - } - IpAddr::V6(_) => { - let data_clone = data.clone(); - data_clone.update_stats(stat_ipv6, count); - } - } + let event = if ip.is_ipv4() { + stats_ipv4 + } else { + stat_ipv6 + }; + data.update_stats(event, count); } #[tracing::instrument(level = "debug")] pub async fn api_validation(request: &HttpRequest, data: &Data>) -> Option { match api_validate_ip(request, data.clone()).await { - Ok(ip) => { api_stat_update(ip, data.torrent_tracker.clone(), StatsEvent::Tcp4ApiHandled, StatsEvent::Tcp6ApiHandled, 1); }, - Err(result) => { return Some(result); } + Ok(ip) => { + api_stat_update( + ip, + Arc::clone(&data.torrent_tracker), + StatsEvent::Tcp4ApiHandled, + StatsEvent::Tcp6ApiHandled, + 1 + ); + None + } + Err(result) => Some(result), } - None } #[tracing::instrument(level = "debug")] @@ -388,14 +319,11 @@ pub async fn api_service_openapi_json() -> HttpResponse #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_parse_body(mut payload: web::Payload) -> Result { - let mut body = web::BytesMut::new(); + let mut body = BytesMut::new(); while let Some(chunk) = payload.next().await { - let chunk = match chunk { - Ok(data) => { data } - Err(_) => { return Err(CustomError::new("chunk error")) } - }; + let chunk = chunk.map_err(|_| CustomError::new("chunk error"))?; - if (body.len() + chunk.len()) > 1_048_576 { + if body.len() + chunk.len() > 1_048_576 { return Err(CustomError::new("chunk size exceeded")); } diff --git a/src/api/api_blacklists.rs b/src/api/api_blacklists.rs index 188f786..bd36a95 100644 --- a/src/api/api_blacklists.rs +++ b/src/api/api_blacklists.rs @@ -14,58 +14,57 @@ use crate::tracker::structs::info_hash::InfoHash; #[tracing::instrument(level = "debug")] pub async fn api_service_blacklist_get(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - match data.torrent_tracker.check_blacklist(info_hash) { - true => { return HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})); } - false => { return HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})); } - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + match data.torrent_tracker.check_blacklist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_blacklists_get(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let blacklists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut blacklist_output = HashMap::new(); + let mut blacklist_output = HashMap::with_capacity(blacklists.len()); for blacklist in blacklists { if blacklist.len() == 40 { - let blacklist_hash = match hex2bin(blacklist.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", blacklist)})) } - }; - - blacklist_output.insert(blacklist_hash, data.torrent_tracker.check_blacklist(blacklist_hash)); + match hex2bin(blacklist.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + blacklist_output.insert(blacklist, data.torrent_tracker.check_blacklist(info_hash)); + } + Err(_) => { + blacklist_output.insert(blacklist, false); + } + } } } @@ -78,68 +77,69 @@ pub async fn api_service_blacklists_get(request: HttpRequest, payload: web::Payl #[tracing::instrument(level = "debug")] pub async fn api_service_blacklist_post(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Add); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.add_blacklist(info_hash) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("info_hash updated {}", info)})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Add); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.add_blacklist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "info_hash updated"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_blacklists_post(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let blacklists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut blacklists_output = HashMap::new(); + let mut blacklists_output = HashMap::with_capacity(blacklists.len()); for info in blacklists { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Add); - } - - match data.torrent_tracker.add_blacklist(info_hash) { - true => { blacklists_output.insert(info_hash, json!({"status": "ok"})); } - false => { blacklists_output.insert(info_hash, json!({"status": "info_hash updated"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Add); + } + + let status = match data.torrent_tracker.add_blacklist(info_hash) { + true => json!({"status": "ok"}), + false => json!({"status": "info_hash updated"}), + }; + blacklists_output.insert(info, status); + } + Err(_) => { + blacklists_output.insert(info, json!({"status": "invalid info_hash"})); + } } } } @@ -153,68 +153,69 @@ pub async fn api_service_blacklists_post(request: HttpRequest, payload: web::Pay #[tracing::instrument(level = "debug")] pub async fn api_service_blacklist_delete(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Remove); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.remove_blacklist(info_hash) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Remove); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.remove_blacklist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_blacklists_delete(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let blacklists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut blacklists_output = HashMap::new(); + let mut blacklists_output = HashMap::with_capacity(blacklists.len()); for info in blacklists { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Remove); - } - - match data.torrent_tracker.remove_blacklist(info_hash) { - true => { blacklists_output.insert(info_hash, json!({"status": "ok"})); } - false => { blacklists_output.insert(info_hash, json!({"status": "unknown info_hash"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_blacklist_update(info_hash, UpdatesAction::Remove); + } + + let status = match data.torrent_tracker.remove_blacklist(info_hash) { + true => json!({"status": "ok"}), + false => json!({"status": "unknown info_hash"}), + }; + blacklists_output.insert(info, status); + } + Err(_) => { + blacklists_output.insert(info, json!({"status": "invalid info_hash"})); + } } } } diff --git a/src/api/api_keys.rs b/src/api/api_keys.rs index f97bb62..c181ff5 100644 --- a/src/api/api_keys.rs +++ b/src/api/api_keys.rs @@ -14,68 +14,63 @@ use crate::tracker::structs::info_hash::InfoHash; #[tracing::instrument(level = "debug")] pub async fn api_service_key_get(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let key = path.into_inner(); - if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})); } - }; - - return match data.torrent_tracker.get_key(key_hash) { - None => { - HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": format!("unknown key_hash {}", key)})) - } - Some((_, timeout)) => { - HttpResponse::Ok().content_type(ContentType::json()).json(json!({ - "status": "ok", - "timeout": timeout - })) - } - } + if key.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})) + let key_hash = match hex2bin(key) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid key_hash"})), + }; + + match data.torrent_tracker.get_key(key_hash) { + None => HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": "unknown key_hash"})), + Some((_, timeout)) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({ + "status": "ok", + "timeout": timeout + })), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_keys_get(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let keys = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut keys_output = HashMap::new(); + let mut keys_output = HashMap::with_capacity(keys.len()); for key in keys { if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})) } - }; - - keys_output.insert(key_hash, match data.torrent_tracker.get_key(key_hash) { - None => { 0u64 } - Some((_, timeout)) => { timeout as u64 } - }); + match hex2bin(key.clone()) { + Ok(hash) => { + let key_hash = InfoHash(hash); + let timeout = data.torrent_tracker.get_key(key_hash) + .map(|(_, timeout)| timeout as u64) + .unwrap_or(0u64); + keys_output.insert(key, timeout); + } + Err(_) => { + keys_output.insert(key, 0u64); + } + } } } @@ -88,68 +83,69 @@ pub async fn api_service_keys_get(request: HttpRequest, payload: web::Payload, d #[tracing::instrument(level = "debug")] pub async fn api_service_key_post(request: HttpRequest, path: web::Path<(String, u64)>, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let (key, timeout) = path.into_inner(); - if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_key_update(key_hash, timeout as i64, UpdatesAction::Add); - } + if key.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})); + } - return match data.torrent_tracker.add_key(key_hash, timeout as i64) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("key_hash updated {}", key)})) } - } + let key_hash = match hex2bin(key) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid key_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_key_update(key_hash, timeout as i64, UpdatesAction::Add); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})) + match data.torrent_tracker.add_key(key_hash, timeout as i64) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "key_hash updated"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_keys_post(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let keys = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut keys_output = HashMap::new(); + let mut keys_output = HashMap::with_capacity(keys.len()); for (key, timeout) in keys { if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_key_update(key_hash, timeout as i64, UpdatesAction::Add); - } - - match data.torrent_tracker.add_key(key_hash, timeout as i64) { - true => { keys_output.insert(key, json!({"status": "ok"})); } - false => { keys_output.insert(key, json!({"status": "key_hash updated"})); } + match hex2bin(key.clone()) { + Ok(hash) => { + let key_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_key_update(key_hash, timeout as i64, UpdatesAction::Add); + } + + let status = match data.torrent_tracker.add_key(key_hash, timeout as i64) { + true => json!({"status": "ok"}), + false => json!({"status": "key_hash updated"}), + }; + keys_output.insert(key, status); + } + Err(_) => { + keys_output.insert(key, json!({"status": "invalid key_hash"})); + } } } } @@ -163,68 +159,69 @@ pub async fn api_service_keys_post(request: HttpRequest, payload: web::Payload, #[tracing::instrument(level = "debug")] pub async fn api_service_key_delete(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let key = path.into_inner(); - if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key {}", key)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_key_update(key_hash, 0i64, UpdatesAction::Remove); - } + if key.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})); + } - return match data.torrent_tracker.remove_key(key_hash) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("unknown key_hash {}", key)})) } - } + let key_hash = match hex2bin(key) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid key"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_key_update(key_hash, 0i64, UpdatesAction::Remove); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})) + match data.torrent_tracker.remove_key(key_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown key_hash"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_keys_delete(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let keys = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut keys_output = HashMap::new(); + let mut keys_output = HashMap::with_capacity(keys.len()); for key_item in keys { if key_item.len() == 40 { - let key_hash = match hex2bin(key_item.clone()) { - Ok(key) => { InfoHash(key) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key {}", key_item)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_key_update(key_hash, 0i64, UpdatesAction::Remove); - } - - match data.torrent_tracker.remove_key(key_hash) { - true => { keys_output.insert(key_hash, json!({"status": "ok"})); } - false => { keys_output.insert(key_hash, json!({"status": "unknown key_hash"})); } + match hex2bin(key_item.clone()) { + Ok(hash) => { + let key_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_key_update(key_hash, 0i64, UpdatesAction::Remove); + } + + let status = match data.torrent_tracker.remove_key(key_hash) { + true => json!({"status": "ok"}), + false => json!({"status": "unknown key_hash"}), + }; + keys_output.insert(key_item, status); + } + Err(_) => { + keys_output.insert(key_item, json!({"status": "invalid key"})); + } } } } diff --git a/src/api/api_stats.rs b/src/api/api_stats.rs index 45ec20f..56398c3 100644 --- a/src/api/api_stats.rs +++ b/src/api/api_stats.rs @@ -9,12 +9,10 @@ use crate::api::structs::query_token::QueryToken; #[tracing::instrument(level = "debug")] pub async fn api_service_stats_get(request: HttpRequest, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } HttpResponse::Ok().content_type(ContentType::json()).json(data.torrent_tracker.get_stats()) } @@ -22,71 +20,67 @@ pub async fn api_service_stats_get(request: HttpRequest, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } - // Get stats let stats = data.torrent_tracker.get_stats(); - // Build Prometheus Output let prometheus_id = &data.torrent_tracker.config.tracker_config.prometheus_id; - let mut string_output = vec![]; + let mut string_output = String::with_capacity(4096); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "torrents", stats.torrents, true, Some(format!("{prometheus_id} gauge metrics").as_str()))); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "torrents_updates", stats.torrents_updates, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "users", stats.users, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "users_updates", stats.users_updates, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "seeds", stats.seeds, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "peers", stats.peers, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "completed", stats.completed, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "whitelist", stats.whitelist, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "whitelist_updates", stats.whitelist_updates, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "blacklist", stats.blacklist, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "blacklist_updates", stats.blacklist_updates, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "keys", stats.keys, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "gauge", "keys_updates", stats.keys_updates, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "torrents", stats.torrents, true, Some(&format!("{prometheus_id} gauge metrics")))); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "torrents_updates", stats.torrents_updates, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "users", stats.users, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "users_updates", stats.users_updates, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "seeds", stats.seeds, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "peers", stats.peers, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "completed", stats.completed, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "whitelist", stats.whitelist, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "whitelist_updates", stats.whitelist_updates, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "blacklist", stats.blacklist, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "blacklist_updates", stats.blacklist_updates, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "keys", stats.keys, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "gauge", "keys_updates", stats.keys_updates, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_not_found", stats.tcp4_not_found, true, Some(format!("{prometheus_id} counter metrics").as_str()))); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_failure", stats.tcp4_failure, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_connections_handled", stats.tcp4_connections_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_api_handled", stats.tcp4_api_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_announces_handled", stats.tcp4_announces_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp4_scrapes_handled", stats.tcp4_scrapes_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_not_found", stats.tcp6_not_found, true, Some(format!("{prometheus_id} counter metrics").as_str()))); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_failure", stats.tcp6_failure, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_connections_handled", stats.tcp6_connections_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_api_handled", stats.tcp6_api_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_announces_handled", stats.tcp6_announces_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "tcp6_scrapes_handled", stats.tcp6_scrapes_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_not_found", stats.tcp4_not_found, true, Some(&format!("{prometheus_id} counter metrics")))); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_failure", stats.tcp4_failure, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_connections_handled", stats.tcp4_connections_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_api_handled", stats.tcp4_api_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_announces_handled", stats.tcp4_announces_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp4_scrapes_handled", stats.tcp4_scrapes_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_not_found", stats.tcp6_not_found, true, Some(&format!("{prometheus_id} counter metrics")))); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_failure", stats.tcp6_failure, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_connections_handled", stats.tcp6_connections_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_api_handled", stats.tcp6_api_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_announces_handled", stats.tcp6_announces_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "tcp6_scrapes_handled", stats.tcp6_scrapes_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp4_bad_request", stats.udp4_bad_request, true, Some(format!("{prometheus_id} counter metrics").as_str()))); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp4_invalid_request", stats.udp4_invalid_request, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp4_connections_handled", stats.udp4_connections_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp4_announces_handled", stats.udp4_announces_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp4_scrapes_handled", stats.udp4_scrapes_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp6_bad_request", stats.udp6_bad_request, true, Some(format!("{prometheus_id} counter metrics").as_str()))); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp6_invalid_request", stats.udp6_invalid_request, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp6_connections_handled", stats.udp6_connections_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp6_announces_handled", stats.udp6_announces_handled, false, None)); - string_output.extend(api_service_prom_generate_line(prometheus_id, "counter", "udp6_scrapes_handled", stats.udp6_scrapes_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp4_bad_request", stats.udp4_bad_request, true, Some(&format!("{prometheus_id} counter metrics")))); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp4_invalid_request", stats.udp4_invalid_request, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp4_connections_handled", stats.udp4_connections_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp4_announces_handled", stats.udp4_announces_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp4_scrapes_handled", stats.udp4_scrapes_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp6_bad_request", stats.udp6_bad_request, true, Some(&format!("{prometheus_id} counter metrics")))); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp6_invalid_request", stats.udp6_invalid_request, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp6_connections_handled", stats.udp6_connections_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp6_announces_handled", stats.udp6_announces_handled, false, None)); + string_output.push_str(&api_service_prom_generate_line(prometheus_id, "counter", "udp6_scrapes_handled", stats.udp6_scrapes_handled, false, None)); - HttpResponse::Ok().content_type(ContentType::plaintext()).body(string_output.join("\n")) + HttpResponse::Ok().content_type(ContentType::plaintext()).body(string_output) } -pub fn api_service_prom_generate_line(id: &str, type_metric: &str, metric: &str, value: i64, without_header: bool, description: Option<&str>) -> Vec +pub fn api_service_prom_generate_line(id: &str, type_metric: &str, metric: &str, value: i64, without_header: bool, description: Option<&str>) -> String { if without_header { - return vec![ - format!("# HELP {}_{} {}", id, type_metric, description.unwrap()).to_string(), - format!("# TYPE {id}_{type_metric} {type_metric}").to_string(), - format!("{id}_{type_metric}{{metric=\"{metric}\"}} {value}").to_string(), - ]; + format!( + "# HELP {}_{} {}\n# TYPE {}_{} {}\n{}_{}{{metric=\"{}\"}} {}\n", + id, type_metric, description.unwrap_or(""), + id, type_metric, type_metric, + id, type_metric, metric, value + ) + } else { + format!("{id}_{type_metric}{{metric=\"{metric}\"}} {value}\n") } - vec![ - format!("{id}_{type_metric}{{metric=\"{metric}\"}} {value}").to_string(), - ] } \ No newline at end of file diff --git a/src/api/api_torrents.rs b/src/api/api_torrents.rs index 8a648f3..34b9711 100644 --- a/src/api/api_torrents.rs +++ b/src/api/api_torrents.rs @@ -1,3 +1,4 @@ +use crate::tracker::structs::peer_id::PeerId; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -16,60 +17,58 @@ use crate::tracker::structs::torrent_entry::TorrentEntry; #[tracing::instrument(level = "debug")] pub async fn api_service_torrent_get(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - match data.torrent_tracker.get_torrent(info_hash) { - None => { return HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})); } - Some(torrent) => { return HttpResponse::Ok().content_type(ContentType::json()).json(api_service_torrents_return_torrent_json(torrent)); } - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + match data.torrent_tracker.get_torrent(info_hash) { + None => HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + Some(torrent) => HttpResponse::Ok().content_type(ContentType::json()).json(api_service_torrents_return_torrent_json(torrent)), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_torrents_get(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let info_hashes = match serde_json::from_slice::>(&body) { - Ok(hash) => { hash } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(hash) => hash, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut torrents_output = HashMap::new(); + let mut torrents_output = HashMap::with_capacity(info_hashes.len()); for info in info_hashes { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - match data.torrent_tracker.get_torrent(info_hash) { - None => {} - Some(torrent) => { torrents_output.insert(info_hash, api_service_torrents_return_torrent_json(torrent)); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + if let Some(torrent) = data.torrent_tracker.get_torrent(info_hash) { + torrents_output.insert(info, api_service_torrents_return_torrent_json(torrent)); + } + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } } } } @@ -83,82 +82,82 @@ pub async fn api_service_torrents_get(request: HttpRequest, payload: web::Payloa #[tracing::instrument(level = "debug")] pub async fn api_service_torrent_post(request: HttpRequest, path: web::Path<(String, u64)>, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let (info, completed) = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - let torrent_entry = TorrentEntry { - seeds: BTreeMap::new(), - peers: BTreeMap::new(), - completed, - updated: std::time::Instant::now(), - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.add_torrent(info_hash, torrent_entry.clone()) { - (_, true) => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - (_, false) => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("info_hash updated {}", info)})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + let torrent_entry = TorrentEntry { + seeds: BTreeMap::new(), + peers: BTreeMap::new(), + completed, + updated: std::time::Instant::now(), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.add_torrent(info_hash, torrent_entry) { + (_, true) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + (_, false) => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "info_hash updated"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_torrents_post(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let info_hashmap = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut torrents_output = HashMap::new(); + let mut torrents_output = HashMap::with_capacity(info_hashmap.len()); for (info, completed) in info_hashmap { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info) })) } - }; - - let torrent_entry = TorrentEntry { - seeds: BTreeMap::new(), - peers: BTreeMap::new(), - completed, - updated: std::time::Instant::now(), - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add); - } - - match data.torrent_tracker.add_torrent(info_hash, torrent_entry.clone()) { - (_, true) => { torrents_output.insert(info_hash, json!({"status": "ok"})); } - (_, false) => { torrents_output.insert(info_hash, json!({"status": "info_hash updated"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + let torrent_entry = TorrentEntry { + seeds: BTreeMap::new(), + peers: BTreeMap::new(), + completed, + updated: std::time::Instant::now(), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add); + } + + let status = match data.torrent_tracker.add_torrent(info_hash, torrent_entry) { + (_, true) => json!({"status": "ok"}), + (_, false) => json!({"status": "info_hash updated"}), + }; + torrents_output.insert(info, status); + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } } } } @@ -172,68 +171,69 @@ pub async fn api_service_torrents_post(request: HttpRequest, payload: web::Paylo #[tracing::instrument(level = "debug")] pub async fn api_service_torrent_delete(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.remove_torrent(info_hash) { - None => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})) } - Some(_) => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.remove_torrent(info_hash) { + None => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + Some(_) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_torrents_delete(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let hashes = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut torrents_output = HashMap::new(); + let mut torrents_output = HashMap::with_capacity(hashes.len()); for info in hashes { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove); - } - - match data.torrent_tracker.remove_torrent(info_hash) { - None => { torrents_output.insert(info_hash, json!({"status": "unknown info_hash"})); } - Some(_) => { torrents_output.insert(info_hash, json!({"status": "ok"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove); + } + + let status = match data.torrent_tracker.remove_torrent(info_hash) { + None => json!({"status": "unknown info_hash"}), + Some(_) => json!({"status": "ok"}), + }; + torrents_output.insert(info, status); + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } } } } @@ -247,37 +247,45 @@ pub async fn api_service_torrents_delete(request: HttpRequest, payload: web::Pay #[tracing::instrument(level = "debug")] pub fn api_service_torrents_return_torrent_json(torrent: TorrentEntry) -> Value { - let seeds = torrent.seeds.iter().map(|(peer_id, torrent_peer)| { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() - torrent_peer.updated.elapsed().as_millis(); - let timestamp_calc: f64 = timestamp as f64 / 2_f64; - let timestamp_final = (timestamp_calc.round() * 2_f64) as u64; + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + + let process_seed = |(peer_id, torrent_peer): (&PeerId, &crate::tracker::structs::torrent_peer::TorrentPeer)| { + let elapsed_ms = torrent_peer.updated.elapsed().as_millis(); + let timestamp = now.saturating_sub(elapsed_ms); + let timestamp_final = ((timestamp as f64 / 2.0).round() * 2.0) as u64; + json!({ - "peer_id": peer_id.clone(), - "peer_addr": torrent_peer.peer_addr.clone(), + "peer_id": peer_id.0, + "peer_addr": torrent_peer.peer_addr, "updated": timestamp_final, "uploaded": torrent_peer.uploaded.0 as u64, "downloaded": torrent_peer.downloaded.0 as u64, "left": torrent_peer.left.0 as u64, }) - }).collect::>(); + }; + + let process_peer = |(peer_id, torrent_peer): (&PeerId, &crate::tracker::structs::torrent_peer::TorrentPeer)| { + let elapsed_ms = torrent_peer.updated.elapsed().as_millis(); + let timestamp = now.saturating_sub(elapsed_ms); + let timestamp_final = ((timestamp as f64 / 2.0).round() * 2.0) as u64; - let peers = torrent.peers.iter().map(|(peer_id, torrent_peer)| { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() - torrent_peer.updated.elapsed().as_millis(); - let timestamp_calc: f64 = timestamp as f64 / 2_f64; - let timestamp_final = (timestamp_calc.round() * 2_f64) as u64; json!({ - "peer_id": peer_id.clone(), - "peer_addr": torrent_peer.peer_addr.clone(), + "peer_id": peer_id.0, + "peer_addr": torrent_peer.peer_addr, "updated": timestamp_final, "uploaded": torrent_peer.uploaded.0 as u64, "downloaded": torrent_peer.downloaded.0 as u64, "left": torrent_peer.left.0 as u64, }) - }).collect::>(); + }; + + let seeds: Vec = torrent.seeds.iter().map(process_seed).collect(); + let peers: Vec = torrent.peers.iter().map(process_peer).collect(); + + let elapsed_ms = torrent.updated.elapsed().as_millis(); + let timestamp = now.saturating_sub(elapsed_ms); + let timestamp_final = ((timestamp as f64 / 2.0).round() * 2.0) as u64; - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() - torrent.updated.elapsed().as_millis(); - let timestamp_calc: f64 = timestamp as f64 / 2_f64; - let timestamp_final = (timestamp_calc.round() * 2_f64) as u64; json!({ "status": "ok", "seeds": seeds, diff --git a/src/api/api_users.rs b/src/api/api_users.rs index 4308400..be21f9f 100644 --- a/src/api/api_users.rs +++ b/src/api/api_users.rs @@ -15,49 +15,49 @@ use crate::tracker::enums::updates_action::UpdatesAction; use crate::tracker::structs::user_entry_item::UserEntryItem; use crate::tracker::structs::user_id::UserId; +lazy_static::lazy_static! { + static ref UUID_REGEX: Regex = Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap(); +} + #[tracing::instrument(level = "debug")] pub async fn api_service_user_get(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let id = path.into_inner(); - let (status_code, data) = api_service_users_return_json(id, data.clone()); + let (status_code, data) = api_service_users_return_json(id, data); match status_code { - StatusCode::OK => { HttpResponse::Ok().content_type(ContentType::json()).json(data) } - _ => { HttpResponse::NotFound().content_type(ContentType::json()).json(data) } + StatusCode::OK => HttpResponse::Ok().content_type(ContentType::json()).json(data), + _ => HttpResponse::NotFound().content_type(ContentType::json()).json(data), } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_users_get(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let ids = match serde_json::from_slice::>(&body) { - Ok(id) => { id } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(id) => id, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut users_output = HashMap::new(); + let mut users_output = HashMap::with_capacity(ids.len()); for id in ids { if id.len() == 40 { - let (_, data) = api_service_users_return_json(id.clone(), data.clone()); - users_output.insert(id.clone(), data); + let (_, user_data) = api_service_users_return_json(id.clone(), Data::clone(&data)); + users_output.insert(id, user_data); } } @@ -70,93 +70,83 @@ pub async fn api_service_users_get(request: HttpRequest, payload: web::Payload, #[tracing::instrument(level = "debug")] pub async fn api_service_user_post(request: HttpRequest, path: web::Path<(String, String, u64, u64, u64, u64, u8)>, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let (id, key, uploaded, downloaded, completed, updated, active) = path.into_inner(); - if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { UserId(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})); } - }; - - let mut user_entry = UserEntryItem { - key: key_hash, - user_id: None, - user_uuid: None, - uploaded, - downloaded, - completed, - updated, - active, - torrents_active: BTreeMap::new(), - }; + if key.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})); + } - match data.torrent_tracker.config.database_structure.clone().users.id_uuid { - true => { - let regex_check = Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap(); - if !regex_check.is_match(id.as_str()) { - return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid uuid {}", id)})); - } - user_entry.user_uuid = Some(id.to_lowercase()); - } - false => { - match id.parse::() { - Ok(data) => { user_entry.user_id = Some(data); } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid id {}", id)})); } - } - } - } + let key_hash = match hex2bin(key) { + Ok(hash) => UserId(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid key_hash"})), + }; - let id_data: &[u8] = id.as_bytes(); - let mut hasher = Sha1::new(); - hasher.update(id_data); - let id_hash = <[u8; 20]>::try_from(hasher.finalize().as_slice()).unwrap(); + let mut user_entry = UserEntryItem { + key: key_hash, + user_id: None, + user_uuid: None, + uploaded, + downloaded, + completed, + updated, + active, + torrents_active: BTreeMap::new(), + }; - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_user_update(UserId(id_hash), user_entry.clone(), UpdatesAction::Add); + let id_hash = if data.torrent_tracker.config.database_structure.users.id_uuid { + if !UUID_REGEX.is_match(&id) { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid uuid"})); } - - return match data.torrent_tracker.add_user(UserId(id_hash), user_entry.clone()) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": format!("user_hash added {}", UserId(id_hash))})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("user_hash updated {}", UserId(id_hash))})) } + user_entry.user_uuid = Some(id.to_lowercase()); + hash_id(&id) + } else { + match id.parse::() { + Ok(user_id) => { + user_entry.user_id = Some(user_id); + hash_id(&id) + } + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid id"})), } + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_user_update(UserId(id_hash), user_entry.clone(), UpdatesAction::Add); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad key_hash"})) + match data.torrent_tracker.add_user(UserId(id_hash), user_entry) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "user_hash added"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "user_hash updated"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_users_post(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let hashes = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut users_output = HashMap::new(); - let regex_check = Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap(); + let mut users_output = HashMap::with_capacity(hashes.len()); for (id, key, uploaded, downloaded, completed, updated, active) in hashes { if key.len() == 40 { - let key_hash = match hex2bin(key.clone()) { - Ok(hash) => { UserId(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid key_hash {}", key)})); } + let key_hash = match hex2bin(key) { + Ok(hash) => UserId(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid key_hash"})), }; let mut user_entry = UserEntryItem { @@ -171,34 +161,31 @@ pub async fn api_service_users_post(request: HttpRequest, payload: web::Payload, torrents_active: BTreeMap::new(), }; - match data.torrent_tracker.config.database_structure.clone().users.id_uuid { - true => { - if !regex_check.is_match(id.as_str()) { - return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid uuid {}", id)})); - } - user_entry.user_uuid = Some(id.to_lowercase()); + let id_hash = if data.torrent_tracker.config.database_structure.users.id_uuid { + if !UUID_REGEX.is_match(&id) { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid uuid"})); } - false => { - match id.parse::() { - Ok(data) => { user_entry.user_id = Some(data); } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid id {}", id)})); } + user_entry.user_uuid = Some(id.to_lowercase()); + hash_id(&id) + } else { + match id.parse::() { + Ok(user_id) => { + user_entry.user_id = Some(user_id); + hash_id(&id) } + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid id"})), } - } - - let id_data: &[u8] = id.as_bytes(); - let mut hasher = Sha1::new(); - hasher.update(id_data); - let id_hash = <[u8; 20]>::try_from(hasher.finalize().as_slice()).unwrap(); + }; - if data.torrent_tracker.config.database.clone().persistent { + if data.torrent_tracker.config.database.persistent { let _ = data.torrent_tracker.add_user_update(UserId(id_hash), user_entry.clone(), UpdatesAction::Add); } - match data.torrent_tracker.add_user(UserId(id_hash), user_entry.clone()) { - true => { users_output.insert(UserId(id_hash), json!({"status": "user_hash added"})); } - false => { users_output.insert(UserId(id_hash), json!({"status": "user_hash updated"})); } - } + let status = match data.torrent_tracker.add_user(UserId(id_hash), user_entry) { + true => json!({"status": "user_hash added"}), + false => json!({"status": "user_hash updated"}), + }; + users_output.insert(id, status); } } @@ -211,73 +198,70 @@ pub async fn api_service_users_post(request: HttpRequest, payload: web::Payload, #[tracing::instrument(level = "debug")] pub async fn api_service_user_delete(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let id = path.into_inner(); - if id.len() == 40 { - let id_hash = match hex2bin(id.clone()) { - Ok(hash) => { UserId(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid user_hash {}", id)})); } - }; + if id.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad user_hash"})); + } - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_user_update(id_hash, UserEntryItem { - key: UserId([0u8; 20]), - user_id: None, - user_uuid: None, - uploaded: 0, - downloaded: 0, - completed: 0, - updated: 0, - active: 0, - torrents_active: Default::default(), - }, UpdatesAction::Remove); - } + let id_hash = match hex2bin(id) { + Ok(hash) => UserId(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid user_hash"})), + }; - return match data.torrent_tracker.remove_user(id_hash) { - None => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("unknown user_hash {}", id)})) } - Some(_) => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - } + if data.torrent_tracker.config.database.persistent { + let empty_user = UserEntryItem { + key: UserId([0u8; 20]), + user_id: None, + user_uuid: None, + uploaded: 0, + downloaded: 0, + completed: 0, + updated: 0, + active: 0, + torrents_active: BTreeMap::new(), + }; + let _ = data.torrent_tracker.add_user_update(id_hash, empty_user, UpdatesAction::Remove); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad user_hash"})) + match data.torrent_tracker.remove_user(id_hash) { + None => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown user_hash"})), + Some(_) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_users_delete(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let ids = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut users_output = HashMap::new(); + let mut users_output = HashMap::with_capacity(ids.len()); for id in ids { if id.len() == 40 { let id_hash = match hex2bin(id.clone()) { - Ok(hash) => { UserId(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid user_hash {}", id)})) } + Ok(hash) => UserId(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid user_hash"})), }; - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_user_update(id_hash, UserEntryItem { + if data.torrent_tracker.config.database.persistent { + let empty_user = UserEntryItem { key: UserId([0u8; 20]), user_id: None, user_uuid: None, @@ -286,14 +270,16 @@ pub async fn api_service_users_delete(request: HttpRequest, payload: web::Payloa completed: 0, updated: 0, active: 0, - torrents_active: Default::default(), - }, UpdatesAction::Remove); + torrents_active: BTreeMap::new(), + }; + let _ = data.torrent_tracker.add_user_update(id_hash, empty_user, UpdatesAction::Remove); } - match data.torrent_tracker.remove_user(id_hash) { - None => { users_output.insert(id_hash, json!({"status": "unknown user_hash"})); } - Some(_) => { users_output.insert(id_hash, json!({"status": "ok"})); } - } + let status = match data.torrent_tracker.remove_user(id_hash) { + None => json!({"status": "unknown user_hash"}), + Some(_) => json!({"status": "ok"}), + }; + users_output.insert(id, status); } } @@ -306,56 +292,44 @@ pub async fn api_service_users_delete(request: HttpRequest, payload: web::Payloa #[tracing::instrument(level = "debug")] pub fn api_service_users_return_json(id: String, data: Data>) -> (StatusCode, Value) { - match data.torrent_tracker.config.database_structure.clone().users.id_uuid { - true => { - let uuid_data: &[u8] = id.as_bytes(); - let mut hasher = Sha1::new(); - hasher.update(uuid_data); - let uuid_hashed = <[u8; 20]>::try_from(hasher.finalize().as_slice()).unwrap(); - - match data.torrent_tracker.get_user(UserId(uuid_hashed)) { - None => { - (StatusCode::NOT_FOUND, json!({"status": "unknown user_hash"})) - } - Some(user_data) => { - (StatusCode::OK, json!({ - "status": "ok", - "uuid": user_data.user_uuid, - "key": user_data.key, - "uploaded": user_data.uploaded, - "downloaded": user_data.downloaded, - "completed": user_data.completed, - "updated": user_data.updated, - "active": user_data.active, - "torrents_active": user_data.torrents_active - })) - } - } - } - false => { - let id_data: &[u8] = id.as_bytes(); - let mut hasher = Sha1::new(); - hasher.update(id_data); - let id_hashed = <[u8; 20]>::try_from(hasher.finalize().as_slice()).unwrap(); - - match data.torrent_tracker.get_user(UserId(id_hashed)) { - None => { - (StatusCode::NOT_FOUND, json!({"status": "unknown user_hash"})) - } - Some(user_data) => { - (StatusCode::OK, json!({ - "status": "ok", - "id": user_data.user_id, - "key": user_data.key, - "uploaded": user_data.uploaded, - "downloaded": user_data.downloaded, - "completed": user_data.completed, - "updated": user_data.updated, - "active": user_data.active, - "torrents_active": user_data.torrents_active - })) - } - } + let id_hash = hash_id(&id); + let uses_uuid = data.torrent_tracker.config.database_structure.users.id_uuid; + + match data.torrent_tracker.get_user(UserId(id_hash)) { + None => (StatusCode::NOT_FOUND, json!({"status": "unknown user_hash"})), + Some(user_data) => { + let response = if uses_uuid { + json!({ + "status": "ok", + "uuid": user_data.user_uuid, + "key": user_data.key, + "uploaded": user_data.uploaded, + "downloaded": user_data.downloaded, + "completed": user_data.completed, + "updated": user_data.updated, + "active": user_data.active, + "torrents_active": user_data.torrents_active + }) + } else { + json!({ + "status": "ok", + "id": user_data.user_id, + "key": user_data.key, + "uploaded": user_data.uploaded, + "downloaded": user_data.downloaded, + "completed": user_data.completed, + "updated": user_data.updated, + "active": user_data.active, + "torrents_active": user_data.torrents_active + }) + }; + (StatusCode::OK, response) } } +} + +fn hash_id(id: &str) -> [u8; 20] { + let mut hasher = Sha1::new(); + hasher.update(id.as_bytes()); + <[u8; 20]>::try_from(hasher.finalize().as_slice()).unwrap() } \ No newline at end of file diff --git a/src/api/api_whitelists.rs b/src/api/api_whitelists.rs index 5e80ae3..c192bf1 100644 --- a/src/api/api_whitelists.rs +++ b/src/api/api_whitelists.rs @@ -10,63 +10,61 @@ use crate::api::structs::query_token::QueryToken; use crate::common::common::hex2bin; use crate::tracker::enums::updates_action::UpdatesAction; use crate::tracker::structs::info_hash::InfoHash; -use crate::tracker::structs::torrent_entry::TorrentEntry; #[tracing::instrument(level = "debug")] pub async fn api_service_whitelist_get(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - match data.torrent_tracker.check_whitelist(info_hash) { - true => { return HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})); } - false => { return HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})); } - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + match data.torrent_tracker.check_whitelist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_whitelists_get(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let whitelists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut whitelist_output = HashMap::new(); + let mut whitelist_output = HashMap::with_capacity(whitelists.len()); for whitelist in whitelists { if whitelist.len() == 40 { - let whitelist_hash = match hex2bin(whitelist.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", whitelist)})) } - }; - - whitelist_output.insert(whitelist_hash, data.torrent_tracker.check_whitelist(whitelist_hash)); + match hex2bin(whitelist.clone()) { + Ok(hash) => { + let whitelist_hash = InfoHash(hash); + whitelist_output.insert(whitelist, data.torrent_tracker.check_whitelist(whitelist_hash)); + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } + } } } @@ -79,68 +77,69 @@ pub async fn api_service_whitelists_get(request: HttpRequest, payload: web::Payl #[tracing::instrument(level = "debug")] pub async fn api_service_whitelist_post(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Add); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.add_whitelist(info_hash) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("info_hash updated {}", info)})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Add); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.add_whitelist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "info_hash updated"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_whitelists_post(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let whitelists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut whitelists_output = HashMap::new(); + let mut whitelists_output = HashMap::with_capacity(whitelists.len()); for info in whitelists { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Add); - } - - match data.torrent_tracker.add_whitelist(info_hash) { - true => { whitelists_output.insert(info_hash, json!({"status": "ok"})); } - false => { whitelists_output.insert(info_hash, json!({"status": "info_hash updated"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Add); + } + + let status = match data.torrent_tracker.add_whitelist(info_hash) { + true => json!({"status": "ok"}), + false => json!({"status": "info_hash updated"}), + }; + whitelists_output.insert(info, status); + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } } } } @@ -154,68 +153,69 @@ pub async fn api_service_whitelists_post(request: HttpRequest, payload: web::Pay #[tracing::instrument(level = "debug")] pub async fn api_service_whitelist_delete(request: HttpRequest, path: web::Path, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let info = path.into_inner(); - if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})); } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove); - } + if info.len() != 40 { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})); + } - return match data.torrent_tracker.remove_whitelist(info_hash) { - true => { HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})) } - false => { HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": format!("unknown info_hash {}", info)})) } - } + let info_hash = match hex2bin(info) { + Ok(hash) => InfoHash(hash), + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})), + }; + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Remove); } - HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad info_hash"})) + match data.torrent_tracker.remove_whitelist(info_hash) { + true => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})), + false => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})), + } } #[tracing::instrument(skip(payload), level = "debug")] pub async fn api_service_whitelists_delete(request: HttpRequest, payload: web::Payload, data: Data>) -> HttpResponse { - // Validate client if let Some(error_return) = api_validation(&request, &data).await { return error_return; } - // Parse the Params let params = web::Query::::from_query(request.query_string()).unwrap(); - if let Some(response) = api_service_token(params.token.clone(), data.torrent_tracker.config.clone()).await { return response; } + if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; } let body = match api_parse_body(payload).await { - Ok(data) => { data } - Err(error) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})); } + Ok(data) => data, + Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})), }; let whitelists = match serde_json::from_slice::>(&body) { - Ok(data) => { data } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})); } + Ok(data) => data, + Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})), }; - let mut whitelists_output = HashMap::new(); + let mut whitelists_output = HashMap::with_capacity(whitelists.len()); for info in whitelists { if info.len() == 40 { - let info_hash = match hex2bin(info.clone()) { - Ok(hash) => { InfoHash(hash) } - Err(_) => { return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": format!("invalid info_hash {}", info)})) } - }; - - if data.torrent_tracker.config.database.clone().persistent { - let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Remove); - } - - match data.torrent_tracker.remove_whitelist(info_hash) { - true => { whitelists_output.insert(info_hash, json!({"status": "ok"})); } - false => { whitelists_output.insert(info_hash, json!({"status": "unknown info_hash"})); } + match hex2bin(info.clone()) { + Ok(hash) => { + let info_hash = InfoHash(hash); + + if data.torrent_tracker.config.database.persistent { + let _ = data.torrent_tracker.add_whitelist_update(info_hash, UpdatesAction::Remove); + } + + let status = match data.torrent_tracker.remove_whitelist(info_hash) { + true => json!({"status": "ok"}), + false => json!({"status": "unknown info_hash"}), + }; + whitelists_output.insert(info, status); + } + Err(_) => { + return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"})) + } } } } diff --git a/src/common/common.rs b/src/common/common.rs index 31a987d..b8eaecc 100644 --- a/src/common/common.rs +++ b/src/common/common.rs @@ -12,48 +12,46 @@ use crate::common::structs::custom_error::CustomError; use crate::config::structs::configuration::Configuration; pub fn parse_query(query: Option) -> Result>>, CustomError> { - let mut queries: HashMap>> = HashMap::new(); - match query { - None => {} - Some(result) => { - let split_raw_query: Vec<&str> = result.split('&').collect(); - for query_item in split_raw_query { - if !query_item.is_empty() { - if query_item.contains('=') { - let key_name_raw = query_item.split('=').collect::>()[0]; - let key_name = percent_encoding::percent_decode_str(key_name_raw).decode_utf8_lossy().to_lowercase(); - if !key_name.is_empty() { - let value_data_raw = query_item.split('=').collect::>()[1]; - let value_data = percent_encoding::percent_decode_str(value_data_raw).collect::>(); - match queries.get(&key_name) { - None => { - let query: Vec> = vec![value_data]; - let _ = queries.insert(key_name, query); - } - Some(result) => { - let mut result_mut = result.clone(); - result_mut.push(value_data); - let _ = queries.insert(key_name, result_mut); - } - } - } - } else { - let key_name = percent_encoding::percent_decode_str(query_item).decode_utf8_lossy().to_lowercase(); - if !key_name.is_empty() { - match queries.get(&key_name) { - None => { - let query: Vec> = vec![]; - let _ = queries.insert(key_name, query); - } - Some(result) => { - let mut result_mut = result.clone(); - result_mut.push(vec![]); - let _ = queries.insert(key_name, result.clone()); - } - } - } - } + let mut queries = HashMap::new(); + + if let Some(result) = query { + for query_item in result.split('&') { + if query_item.is_empty() { + continue; + } + + if let Some(equal_pos) = query_item.find('=') { + let (key_part, value_part) = query_item.split_at(equal_pos); + let key_name_raw = key_part; + let value_data_raw = &value_part[1..]; + + let key_name = percent_encoding::percent_decode_str(key_name_raw) + .decode_utf8_lossy() + .to_lowercase(); + + if key_name.is_empty() { + continue; + } + + let value_data = percent_encoding::percent_decode_str(value_data_raw).collect::>(); + + queries + .entry(key_name) + .or_insert_with(Vec::new) + .push(value_data); + } else { + let key_name = percent_encoding::percent_decode_str(query_item) + .decode_utf8_lossy() + .to_lowercase(); + + if key_name.is_empty() { + continue; } + + queries + .entry(key_name) + .or_insert_with(Vec::new) + .push(Vec::new()); } } } @@ -63,13 +61,10 @@ pub fn parse_query(query: Option) -> Result> pub fn udp_check_host_and_port_used(bind_address: String) { if cfg!(target_os = "windows") { - match std::net::UdpSocket::bind(&bind_address) { - Ok(e) => e, - Err(data) => { - sentry::capture_error(&data); - panic!("Unable to bind to {} ! Exiting...", &bind_address); - } - }; + if let Err(data) = std::net::UdpSocket::bind(&bind_address) { + sentry::capture_error(&data); + panic!("Unable to bind to {} ! Exiting...", &bind_address); + } } } @@ -79,15 +74,18 @@ pub(crate) fn bin2hex(data: &[u8; 20], f: &mut Formatter) -> fmt::Result { write!(f, "{}", std::str::from_utf8(&chars).unwrap()) } -pub fn hex2bin(data: String) -> Result<[u8; 20], CustomError> -{ - match hex::decode(data) { - Ok(hash_result) => { Ok(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(data) => { +pub fn hex2bin(data: String) -> Result<[u8; 20], CustomError> { + hex::decode(data) + .map_err(|data| { sentry::capture_error(&data); - Err(CustomError::new("error converting hex to bin")) - } - } + CustomError::new("error converting hex to bin") + }) + .and_then(|hash_result| { + hash_result + .get(..20) + .and_then(|slice| slice.try_into().ok()) + .ok_or_else(|| CustomError::new("invalid hex length")) + }) } pub fn print_type(_: &T) { @@ -98,18 +96,16 @@ pub fn return_type(_: &T) -> String { format!("{:?}", std::any::type_name::()) } -pub fn equal_string_check(source: &String, check: &String) -> bool -{ - if *source.to_string() == format!("{check:?}") { +pub fn equal_string_check(source: &str, check: &str) -> bool { + if source == check { return true; } println!("Source: {source}"); - println!("Check: {check:?}"); + println!("Check: {check}"); false } -pub fn setup_logging(config: &Configuration) -{ +pub fn setup_logging(config: &Configuration) { let level = match config.log_level.as_str() { "off" => log::LevelFilter::Off, "trace" => log::LevelFilter::Trace, @@ -129,7 +125,7 @@ pub fn setup_logging(config: &Configuration) .warn(Color::Yellow) .error(Color::Red); - if let Err(_err) = fern::Dispatch::new() + fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( "{} [{:width$}][{}] {}", @@ -143,47 +139,33 @@ pub fn setup_logging(config: &Configuration) .level(level) .chain(std::io::stdout()) .apply() - { - panic!("Failed to initialize logging.") - } + .unwrap_or_else(|_| panic!("Failed to initialize logging.")); + info!("logging initialized."); } pub async fn current_time() -> u64 { SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH).unwrap() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("System time before UNIX epoch") .as_secs() } pub async fn convert_int_to_bytes(number: &u64) -> Vec { - let mut return_data: Vec = Vec::new(); - for i in 1..8 { - if number < &256u64.pow(i) { - let start: usize = 16usize - i as usize; - return_data.extend(number.to_be_bytes()[start..8].iter()); - return return_data; - } - } - return_data + let bytes = number.to_be_bytes(); + let leading_zeros = number.leading_zeros() as usize / 8; + bytes[leading_zeros..].to_vec() } -pub async fn convert_bytes_to_int(array: &Vec) -> u64 { - let mut array_fixed: Vec = Vec::new(); - let size = 8 - array.len(); - array_fixed.resize(size, 0); - array_fixed.extend(array); - let mut rdr = Cursor::new(array_fixed); - rdr.read_u64::().unwrap() +pub async fn convert_bytes_to_int(array: &[u8]) -> u64 { + let mut array_fixed = [0u8; 8]; + let start_idx = 8 - array.len(); + array_fixed[start_idx..].copy_from_slice(array); + Cursor::new(array_fixed).read_u64::().unwrap() } -pub async fn shutdown_waiting(timeout: Duration, shutdown_handler: Shutdown) -> bool -{ - match future::timeout(timeout, shutdown_handler.handle()).await { - Ok(_) => { - true - } - Err(_) => { - false - } - } +pub async fn shutdown_waiting(timeout: Duration, shutdown_handler: Shutdown) -> bool { + future::timeout(timeout, shutdown_handler.handle()) + .await + .is_ok() } \ No newline at end of file diff --git a/src/config/impls/configuration.rs b/src/config/impls/configuration.rs index e94640d..264cdf3 100644 --- a/src/config/impls/configuration.rs +++ b/src/config/impls/configuration.rs @@ -122,7 +122,8 @@ impl Configuration { UdpTrackersConfig { enabled: true, bind_address: String::from("0.0.0.0:6969"), - threads: available_parallelism().unwrap().get() as u64, + udp_threads: 4, + worker_threads: available_parallelism().unwrap().get(), receive_buffer_size: 134217728, send_buffer_size: 67108864, reuse_address: true @@ -149,7 +150,6 @@ impl Configuration { #[tracing::instrument(level = "debug")] pub fn env_overrides(config: &mut Configuration) -> &mut Configuration { - // Config if let Ok(value) = env::var("LOG_LEVEL") { config.log_level = value; } if let Ok(value) = env::var("LOG_CONSOLE_INTERVAL") { config.log_console_interval = value.parse::().unwrap_or(60u64); } @@ -454,8 +454,20 @@ impl Configuration { if let Ok(value) = env::var(format!("UDP_{udp_iteration}_BIND_ADDRESS")) { block.bind_address = value; } - if let Ok(value) = env::var(format!("UDP_{udp_iteration}_THREADS")) { - block.threads = value.parse::().unwrap_or(available_parallelism().unwrap().get() as u64); + if let Ok(value) = env::var(format!("UDP_{udp_iteration}_UDP_THREADS")) { + block.udp_threads = value.parse::().unwrap_or(2); + } + if let Ok(value) = env::var(format!("UDP_{udp_iteration}_WORKER_THREADS")) { + block.worker_threads = value.parse::().unwrap_or(available_parallelism().unwrap().get()); + } + if let Ok(value) = env::var(format!("UDP_{udp_iteration}_RECEIVE_BUFFER_SIZE")) { + block.receive_buffer_size = value.parse::().unwrap_or(134217728); + } + if let Ok(value) = env::var(format!("UDP_{udp_iteration}_SEND_BUFFER_SIZE")) { + block.send_buffer_size = value.parse::().unwrap_or(67108864); + } + if let Ok(value) = env::var(format!("UDP_{udp_iteration}_REUSE_ADDRESS")) { + block.reuse_address = match value.as_str() { "true" => { true } "false" => { false } _ => { true } }; } } } diff --git a/src/config/structs/udp_trackers_config.rs b/src/config/structs/udp_trackers_config.rs index 5b3c775..857247d 100644 --- a/src/config/structs/udp_trackers_config.rs +++ b/src/config/structs/udp_trackers_config.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; pub struct UdpTrackersConfig { pub enabled: bool, pub bind_address: String, - pub threads: u64, + pub udp_threads: usize, + pub worker_threads: usize, pub receive_buffer_size: usize, pub send_buffer_size: usize, pub reuse_address: bool diff --git a/src/http/http.rs b/src/http/http.rs index 7d89526..cc09948 100644 --- a/src/http/http.rs +++ b/src/http/http.rs @@ -257,7 +257,6 @@ pub async fn http_service_announce_userkey(request: HttpRequest, path: web::Path #[tracing::instrument(level = "debug")] pub async fn http_service_announce(request: HttpRequest, data: Data>) -> HttpResponse { - // Validate the IP address let ip = match http_validate_ip(request.clone(), data.clone()).await { Ok(ip) => { http_stat_update(ip, data.torrent_tracker.clone(), StatsEvent::Tcp4AnnouncesHandled, StatsEvent::Tcp6AnnouncesHandled, 1); @@ -328,13 +327,34 @@ pub async fn http_service_announce_handler(request: HttpRequest, ip: IpAddr, dat IpAddr::V4(_) => { if announce_unwrapped.left != 0 { let seeds = data.get_peers( - torrent_entry.seeds.clone(), + &torrent_entry.seeds, TorrentPeersType::IPv4, Some(ip), 72 ); - if seeds.is_some() { - for (_, torrent_peer) in seeds.unwrap().iter() { + for (_, torrent_peer) in seeds.iter() { + let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { + Ok(ip) => { ip } + Err(e) => { + error!("[IPV4 Error] {} - {}", torrent_peer.peer_addr.ip(), e); + return HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { + "failure reason" => ben_bytes!(e.to_string()) + }.encode()); + } + }; + let _ = peers_list.write(&u32::from(peer_pre_parse).to_be_bytes()); + peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); + } + } + if peers_list.len() != 72 { + let peers = data.get_peers( + &torrent_entry.peers, + TorrentPeersType::IPv4, + Some(ip), + 72 + ); + for (_, torrent_peer) in peers.iter() { + if peers_list.len() != 72 { let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { Ok(ip) => { ip } Err(e) => { @@ -346,34 +366,9 @@ pub async fn http_service_announce_handler(request: HttpRequest, ip: IpAddr, dat }; let _ = peers_list.write(&u32::from(peer_pre_parse).to_be_bytes()); peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); + continue; } - } - } - if peers_list.len() != 72 { - let peers = data.get_peers( - torrent_entry.peers.clone(), - TorrentPeersType::IPv4, - Some(ip), - 72 - ); - if peers.is_some() { - for (_, torrent_peer) in peers.unwrap().iter() { - if peers_list.len() != 72 { - let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { - Ok(ip) => { ip } - Err(e) => { - error!("[IPV4 Error] {} - {}", torrent_peer.peer_addr.ip(), e); - return HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { - "failure reason" => ben_bytes!(e.to_string()) - }.encode()); - } - }; - let _ = peers_list.write(&u32::from(peer_pre_parse).to_be_bytes()); - peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); - continue; - } - break; - } + break; } } HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { @@ -388,52 +383,48 @@ pub async fn http_service_announce_handler(request: HttpRequest, ip: IpAddr, dat IpAddr::V6(_) => { if announce_unwrapped.left != 0 { let seeds = data.get_peers( - torrent_entry.seeds.clone(), + &torrent_entry.seeds, + TorrentPeersType::IPv6, + Some(ip), + 72 + ); + for (_, torrent_peer) in seeds.iter() { + let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { + Ok(ip) => { ip } + Err(e) => { + error!("[IPV6 Error] {} - {}", torrent_peer.peer_addr.ip(), e); + return HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { + "failure reason" => ben_bytes!(e.to_string()) + }.encode()); + } + }; + let _ = peers_list.write(&u128::from(peer_pre_parse).to_be_bytes()); + peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); + } + } + if peers_list.len() != 72 { + let peers = data.get_peers( + &torrent_entry.peers, TorrentPeersType::IPv6, Some(ip), 72 ); - if seeds.is_some() { - for (_, torrent_peer) in seeds.unwrap().iter() { + for (_, torrent_peer) in peers.iter() { + if peers_list.len() != 72 { let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { Ok(ip) => { ip } Err(e) => { error!("[IPV6 Error] {} - {}", torrent_peer.peer_addr.ip(), e); return HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { - "failure reason" => ben_bytes!(e.to_string()) - }.encode()); + "failure reason" => ben_bytes!(e.to_string()) + }.encode()); } }; let _ = peers_list.write(&u128::from(peer_pre_parse).to_be_bytes()); peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); + continue; } - } - } - if peers_list.len() != 72 { - let peers = data.get_peers( - torrent_entry.peers.clone(), - TorrentPeersType::IPv6, - Some(ip), - 72 - ); - if peers.is_some() { - for (_, torrent_peer) in peers.unwrap().iter() { - if peers_list.len() != 72 { - let peer_pre_parse = match torrent_peer.peer_addr.ip().to_string().parse::() { - Ok(ip) => { ip } - Err(e) => { - error!("[IPV6 Error] {} - {}", torrent_peer.peer_addr.ip(), e); - return HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { - "failure reason" => ben_bytes!(e.to_string()) - }.encode()); - } - }; - let _ = peers_list.write(&u128::from(peer_pre_parse).to_be_bytes()); - peers_list.write_all(&announce_unwrapped.clone().port.to_be_bytes()).unwrap(); - continue; - } - break; - } + break; } } HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { @@ -454,40 +445,36 @@ pub async fn http_service_announce_handler(request: HttpRequest, ip: IpAddr, dat IpAddr::V4(_) => { if announce_unwrapped.left != 0 { let seeds = data.get_peers( - torrent_entry.seeds.clone(), + &torrent_entry.seeds, TorrentPeersType::IPv4, Some(ip), 72 ); - if seeds.is_some() { - for (peer_id, torrent_peer) in seeds.unwrap().iter() { - peers_list_mut.push(ben_map! { - "peer id" => ben_bytes!(peer_id.to_string()), - "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), - "port" => ben_int!(torrent_peer.peer_addr.port() as i64) - }); - } + for (peer_id, torrent_peer) in seeds.iter() { + peers_list_mut.push(ben_map! { + "peer id" => ben_bytes!(peer_id.to_string()), + "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), + "port" => ben_int!(torrent_peer.peer_addr.port() as i64) + }); } } if peers_list_mut.len() != 72 { let peers = data.get_peers( - torrent_entry.peers.clone(), + &torrent_entry.peers, TorrentPeersType::IPv4, Some(ip), 72 ); - if peers.is_some() { - for (peer_id, torrent_peer) in peers.unwrap().iter() { - if peers_list_mut.len() != 72 { - peers_list_mut.push(ben_map! { - "peer id" => ben_bytes!(peer_id.to_string()), - "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), - "port" => ben_int!(torrent_peer.peer_addr.port() as i64) - }); - continue; - } - break; + for (peer_id, torrent_peer) in peers.iter() { + if peers_list_mut.len() != 72 { + peers_list_mut.push(ben_map! { + "peer id" => ben_bytes!(peer_id.to_string()), + "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), + "port" => ben_int!(torrent_peer.peer_addr.port() as i64) + }); + continue; } + break; } } HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { @@ -502,40 +489,36 @@ pub async fn http_service_announce_handler(request: HttpRequest, ip: IpAddr, dat IpAddr::V6(_) => { if announce_unwrapped.left != 0 { let seeds = data.get_peers( - torrent_entry.seeds.clone(), + &torrent_entry.seeds, TorrentPeersType::IPv6, Some(ip), 72 ); - if seeds.is_some() { - for (peer_id, torrent_peer) in seeds.unwrap().iter() { - peers_list_mut.push(ben_map! { - "peer id" => ben_bytes!(peer_id.to_string()), - "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), - "port" => ben_int!(torrent_peer.peer_addr.port() as i64) - }); - } + for (peer_id, torrent_peer) in seeds.iter() { + peers_list_mut.push(ben_map! { + "peer id" => ben_bytes!(peer_id.to_string()), + "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), + "port" => ben_int!(torrent_peer.peer_addr.port() as i64) + }); } } if peers_list_mut.len() != 72 { let peers = data.get_peers( - torrent_entry.peers.clone(), + &torrent_entry.peers, TorrentPeersType::IPv6, Some(ip), 72 ); - if peers.is_some() { - for (peer_id, torrent_peer) in peers.unwrap().iter() { - if peers_list_mut.len() != 72 { - peers_list_mut.push(ben_map! { - "peer id" => ben_bytes!(peer_id.to_string()), - "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), - "port" => ben_int!(torrent_peer.peer_addr.port() as i64) - }); - continue; - } - break; + for (peer_id, torrent_peer) in peers.iter() { + if peers_list_mut.len() != 72 { + peers_list_mut.push(ben_map! { + "peer id" => ben_bytes!(peer_id.to_string()), + "ip" => ben_bytes!(torrent_peer.peer_addr.ip().to_string()), + "port" => ben_int!(torrent_peer.peer_addr.port() as i64) + }); + continue; } + break; } } HttpResponse::Ok().content_type(ContentType::plaintext()).body(ben_map! { diff --git a/src/main.rs b/src/main.rs index 919b6b0..922cc4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use async_std::task; use clap::Parser; -use futures_util::future::{try_join_all, TryJoinAll}; +use futures_util::future::try_join_all; use log::{error, info}; use parking_lot::deadlock; use sentry::ClientInitGuard; @@ -36,260 +36,339 @@ fn main() -> std::io::Result<()> #[warn(unused_variables)] let _sentry_guard: ClientInitGuard; - if config.sentry_config.clone().enabled { - _sentry_guard = sentry::init((config.sentry_config.clone().dsn, sentry::ClientOptions { + if config.sentry_config.enabled { + _sentry_guard = sentry::init((config.sentry_config.dsn.clone(), sentry::ClientOptions { release: sentry::release_name!(), - debug: config.sentry_config.clone().debug, - sample_rate: config.sentry_config.clone().sample_rate, - max_breadcrumbs: config.sentry_config.clone().max_breadcrumbs, - attach_stacktrace: config.sentry_config.clone().attach_stacktrace, - send_default_pii: config.sentry_config.clone().send_default_pii, - traces_sample_rate: config.sentry_config.clone().traces_sample_rate, + debug: config.sentry_config.debug, + sample_rate: config.sentry_config.sample_rate, + max_breadcrumbs: config.sentry_config.max_breadcrumbs, + attach_stacktrace: config.sentry_config.attach_stacktrace, + send_default_pii: config.sentry_config.send_default_pii, + traces_sample_rate: config.sentry_config.traces_sample_rate, session_mode: sentry::SessionMode::Request, auto_session_tracking: true, ..Default::default() })); } - tokio::runtime::Builder::new_multi_thread() + Builder::new_multi_thread() .enable_all() .build()? .block_on(async { let tracker = Arc::new(TorrentTracker::new(config.clone(), args.create_database).await); - if tracker.config.database.clone().persistent { + let tracker_config = tracker.config.tracker_config.clone(); + let db_config = tracker.config.database.clone(); + + if db_config.persistent { tracker.load_torrents(tracker.clone()).await; - if tracker.config.tracker_config.clone().whitelist_enabled { + + if tracker_config.whitelist_enabled { tracker.load_whitelist(tracker.clone()).await; } - if tracker.config.tracker_config.clone().blacklist_enabled { + if tracker_config.blacklist_enabled { tracker.load_blacklist(tracker.clone()).await; } - if tracker.config.tracker_config.clone().keys_enabled { + if tracker_config.keys_enabled { tracker.load_keys(tracker.clone()).await; } - if tracker.config.tracker_config.clone().users_enabled { + if tracker_config.users_enabled { tracker.load_users(tracker.clone()).await; } - if tracker.config.database.clone().update_peers && !tracker.reset_seeds_peers(tracker.clone()).await { + if db_config.update_peers && !tracker.reset_seeds_peers(tracker.clone()).await { panic!("[RESET SEEDS PEERS] Unable to continue loading"); } } else { - tracker.set_stats(StatsEvent::Completed, config.tracker_config.clone().total_downloads as i64); + tracker.set_stats(StatsEvent::Completed, config.tracker_config.total_downloads as i64); } if args.create_selfsigned { tracker.cert_gen(&args).await; } - if args.export { tracker.export(&args, tracker.clone()).await; } - if args.import { tracker.import(&args, tracker.clone()).await; } let tokio_core = Builder::new_multi_thread().thread_name("core").worker_threads(9).enable_all().build()?; - let tokio_shutdown = Shutdown::new().expect("shutdown creation works on first call"); let deadlocks_handler = tokio_shutdown.clone(); tokio_core.spawn(async move { info!("[BOOT] Starting thread for deadlocks..."); + let mut interval = tokio::time::interval(Duration::from_secs(30)); loop { - if shutdown_waiting(Duration::from_secs(10), deadlocks_handler.clone()).await { - info!("[BOOT] Shutting down thread for deadlocks..."); - return; - } - let deadlocks = deadlock::check_deadlock(); - if deadlocks.is_empty() { - continue; - } - info!("[DEADLOCK] Found {} deadlocks", deadlocks.len()); - for (i, threads) in deadlocks.iter().enumerate() { - info!("[DEADLOCK] #{i}"); - for t in threads { - info!("[DEADLOCK] Thread ID: {:#?}", t.thread_id()); - info!("[DEADLOCK] {:#?}", t.backtrace()); - sentry::capture_message(format!("{:#?}", t.backtrace()).as_str(), sentry::Level::Error); + tokio::select! { + _ = interval.tick() => { + let deadlocks = deadlock::check_deadlock(); + if !deadlocks.is_empty() { + info!("[DEADLOCK] Found {} deadlocks", deadlocks.len()); + for (i, threads) in deadlocks.iter().enumerate() { + info!("[DEADLOCK] #{i}"); + for t in threads { + info!("[DEADLOCK] Thread ID: {:#?}", t.thread_id()); + info!("[DEADLOCK] {:#?}", t.backtrace()); + sentry::capture_message(&format!("{:#?}", t.backtrace()), sentry::Level::Error); + } + } + } + } + _ = deadlocks_handler.handle() => { + info!("[BOOT] Shutting down thread for deadlocks..."); + return; } } } }); - let mut api_handlers = Vec::new(); let mut api_futures = Vec::new(); - let mut apis_handlers = Vec::new(); let mut apis_futures = Vec::new(); + for api_server_object in &config.api_server { if api_server_object.enabled { http_check_host_and_port_used(api_server_object.bind_address.clone()); let address: SocketAddr = api_server_object.bind_address.parse().unwrap(); + + let (handle, future) = api_service( + address, + tracker.clone(), + api_server_object.clone() + ).await; + if api_server_object.ssl { - let (handle, https_api) = api_service( - address, - tracker.clone(), - api_server_object.clone() - ).await; - apis_handlers.push(handle); - apis_futures.push(https_api); + apis_futures.push((handle, future)); } else { - let (handle, http_api) = api_service( - address, - tracker.clone(), - api_server_object.clone() - ).await; - api_handlers.push(handle); - api_futures.push(http_api); + api_futures.push((handle, future)); } } } + if !api_futures.is_empty() { + let (handles, futures): (Vec<_>, Vec<_>) = api_futures.into_iter().unzip(); tokio_core.spawn(async move { - let _ = try_join_all(api_futures).await; + let _ = try_join_all(futures).await; + drop(handles); }); } if !apis_futures.is_empty() { + let (handles, futures): (Vec<_>, Vec<_>) = apis_futures.into_iter().unzip(); tokio_core.spawn(async move { - let _ = try_join_all(apis_futures).await; + let _ = try_join_all(futures).await; + drop(handles); }); } - let mut http_handlers = Vec::new(); let mut http_futures = Vec::new(); - let mut https_handlers = Vec::new(); let mut https_futures = Vec::new(); + for http_server_object in &config.http_server { if http_server_object.enabled { http_check_host_and_port_used(http_server_object.bind_address.clone()); let address: SocketAddr = http_server_object.bind_address.parse().unwrap(); + + let (handle, future) = http_service( + address, + tracker.clone(), + http_server_object.clone() + ).await; + if http_server_object.ssl { - let (handle, https_service) = http_service( - address, - tracker.clone(), - http_server_object.clone() - ).await; - https_handlers.push(handle); - https_futures.push(https_service); + https_futures.push((handle, future)); } else { - let (handle, http_service) = http_service( - address, - tracker.clone(), - http_server_object.clone() - ).await; - http_handlers.push(handle); - http_futures.push(http_service); + http_futures.push((handle, future)); } } } + if !http_futures.is_empty() { + let (handles, futures): (Vec<_>, Vec<_>) = http_futures.into_iter().unzip(); tokio_core.spawn(async move { - let _ = try_join_all(http_futures).await; + let _ = try_join_all(futures).await; + drop(handles); }); } if !https_futures.is_empty() { + let (handles, futures): (Vec<_>, Vec<_>) = https_futures.into_iter().unzip(); tokio_core.spawn(async move { - let _ = try_join_all(https_futures).await; + let _ = try_join_all(futures).await; + drop(handles); }); } let (udp_tx, udp_rx) = tokio::sync::watch::channel(false); let mut udp_tokio_threads = Vec::new(); let mut udp_futures = Vec::new(); + for udp_server_object in &config.udp_server { if udp_server_object.enabled { udp_check_host_and_port_used(udp_server_object.bind_address.clone()); let address: SocketAddr = udp_server_object.bind_address.parse().unwrap(); - let threads: u64 = udp_server_object.threads; - let recv_buffer_size: usize = udp_server_object.receive_buffer_size; - let send_buffer_size: usize = udp_server_object.send_buffer_size; - let reuse_address: bool = udp_server_object.reuse_address; - let tracker_clone = tracker.clone(); - let tokio_udp = Arc::new(Builder::new_multi_thread().thread_name("udp").worker_threads(threads as usize).enable_all().build()?); - udp_futures.push(udp_service(address, threads, recv_buffer_size, send_buffer_size, reuse_address, tracker_clone, udp_rx.clone(), tokio_udp.clone()).await); - udp_tokio_threads.push(tokio_udp.clone()); + + let udp_threads: usize = udp_server_object.udp_threads; + let worker_threads: usize = udp_server_object.worker_threads; + + let tokio_udp = Arc::new(Builder::new_multi_thread() + .thread_name("udp") + .worker_threads(udp_threads) + .enable_all() + .build()?); + + let udp_future = udp_service( + address, + udp_threads, + worker_threads, + udp_server_object.receive_buffer_size, + udp_server_object.send_buffer_size, + udp_server_object.reuse_address, + tracker.clone(), + udp_rx.clone(), + tokio_udp.clone() + ).await; + + udp_futures.push(udp_future); + udp_tokio_threads.push(tokio_udp); } } let stats_handler = tokio_shutdown.clone(); let tracker_spawn_stats = tracker.clone(); - info!("[BOOT] Starting thread for console updates with {} seconds delay...", tracker_spawn_stats.config.log_console_interval); + let console_interval = tracker_spawn_stats.config.log_console_interval; + info!("[BOOT] Starting thread for console updates with {console_interval} seconds delay..."); + tokio_core.spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(console_interval)); + let mut last_udp: Option<(i64,i64,i64,i64,i64,i64,i64)> = None; loop { - tracker_spawn_stats.set_stats(StatsEvent::TimestampSave, chrono::Utc::now().timestamp() + 60i64); - if shutdown_waiting(Duration::from_secs(tracker_spawn_stats.config.log_console_interval), stats_handler.clone()).await { - info!("[BOOT] Shutting down thread for console updates..."); - return; + tokio::select! { + _ = interval.tick() => { + tracker_spawn_stats.set_stats(StatsEvent::TimestampSave, chrono::Utc::now().timestamp() + 60); + let stats = tracker_spawn_stats.get_stats(); + + info!( + "[STATS] Torrents: {} - Updates: {} - Seeds: {} - Peers: {} - Completed: {} | \ + WList: {} - WList Updates: {} - BLists: {} - BLists Updates: {} - Keys: {} - Keys Updates {}", + stats.torrents, stats.torrents_updates, stats.seeds, stats.peers, stats.completed, + stats.whitelist, stats.whitelist_updates, stats.blacklist, stats.blacklist_updates, + stats.keys, stats.keys_updates + ); + + info!( + "[STATS TCP] IPv4: Conn:{} API:{} A:{} S:{} F:{} 404:{} | IPv6: Conn:{} API:{} A:{} S:{} F:{} 404:{}", + stats.tcp4_connections_handled, stats.tcp4_api_handled, stats.tcp4_announces_handled, + stats.tcp4_scrapes_handled, stats.tcp4_failure, stats.tcp4_not_found, + stats.tcp6_connections_handled, stats.tcp6_api_handled, stats.tcp6_announces_handled, + stats.tcp6_scrapes_handled, stats.tcp6_failure, stats.tcp6_not_found + ); + + let now = chrono::Utc::now().timestamp(); + let (udp_c4_ps, udp_a4_ps, udp_s4_ps, udp_c6_ps, udp_a6_ps, udp_s6_ps) = if let Some((t,c4,a4,s4,c6,a6,s6)) = last_udp { + let dt = (now - t).max(1); + ( + (stats.udp4_connections_handled - c4)/dt, + (stats.udp4_announces_handled - a4)/dt, + (stats.udp4_scrapes_handled - s4)/dt, + (stats.udp6_connections_handled - c6)/dt, + (stats.udp6_announces_handled - a6)/dt, + (stats.udp6_scrapes_handled - s6)/dt, + ) + } else { (0,0,0,0,0,0) }; + last_udp = Some((now, stats.udp4_connections_handled, stats.udp4_announces_handled, stats.udp4_scrapes_handled, + stats.udp6_connections_handled, stats.udp6_announces_handled, stats.udp6_scrapes_handled)); + + info!( + "[STATS UDP] IPv4: Conn:{} ({}s) A:{} ({}s) S:{} ({}s) IR:{} BR:{} | IPv6: Conn:{} ({}s) A:{} ({}s) S:{} ({}s) IR:{} BR:{} | Q:{}", + stats.udp4_connections_handled, udp_c4_ps, stats.udp4_announces_handled, udp_a4_ps, stats.udp4_scrapes_handled, udp_s4_ps, + stats.udp4_invalid_request, stats.udp4_bad_request, + stats.udp6_connections_handled, udp_c6_ps, stats.udp6_announces_handled, udp_a6_ps, stats.udp6_scrapes_handled, udp_s6_ps, + stats.udp6_invalid_request, stats.udp6_bad_request, + stats.udp_queue_len + ); + } + _ = stats_handler.handle() => { + info!("[BOOT] Shutting down thread for console updates..."); + return; + } } - - let stats = tracker_spawn_stats.get_stats(); - info!("[STATS] Torrents: {} - Updates: {} - Seeds: {} - Peers: {} - Completed: {}", stats.torrents, stats.torrents_updates, stats.seeds, stats.peers, stats.completed); - info!("[STATS] WList: {} - WList Updates: {} - BLists: {} - BLists Updates: {} - Keys: {} - Keys Updates {}", stats.whitelist, stats.whitelist_updates, stats.blacklist, stats.blacklist_updates, stats.keys, stats.keys_updates); - info!("[STATS TCP IPv4] Connect: {} - API: {} - A: {} - S: {} - F: {} - 404: {}", stats.tcp4_connections_handled, stats.tcp4_api_handled, stats.tcp4_announces_handled, stats.tcp4_scrapes_handled, stats.tcp4_failure, stats.tcp4_not_found); - info!("[STATS TCP IPv6] Connect: {} - API: {} - A: {} - S: {} - F: {} - 404: {}", stats.tcp6_connections_handled, stats.tcp6_api_handled, stats.tcp6_announces_handled, stats.tcp6_scrapes_handled, stats.tcp6_failure, stats.tcp6_not_found); - info!("[STATS UDP IPv4] Connect: {} - A: {} - S: {} - IR: {} - BR: {}", stats.udp4_connections_handled, stats.udp4_announces_handled, stats.udp4_scrapes_handled, stats.udp4_invalid_request, stats.udp4_bad_request); - info!("[STATS UDP IPv6] Connect: {} - A: {} - S: {} - IR: {} - BR: {}", stats.udp6_connections_handled, stats.udp6_announces_handled, stats.udp6_scrapes_handled, stats.udp6_invalid_request, stats.udp6_bad_request); } }); - let (tracker_cleanup_clone, tokio_shutdown_cleanup_clone) = (tracker.clone(), tokio_shutdown.clone()); - info!("[BOOT] Starting thread for peers cleanup with {} seconds delay...", tracker_cleanup_clone.config.tracker_config.clone().peers_cleanup_interval); + let tracker_cleanup_clone = tracker.clone(); + let cleanup_handler = tokio_shutdown.clone(); + let cleanup_interval = tracker_cleanup_clone.config.tracker_config.peers_cleanup_interval; + info!("[BOOT] Starting thread for peers cleanup with {cleanup_interval} seconds delay..."); + + let peers_timeout = tracker_cleanup_clone.config.tracker_config.peers_timeout; + let persistent = tracker_cleanup_clone.config.database.persistent; + let torrents_sharding = tracker_cleanup_clone.torrents_sharding.clone(); + tokio_core.spawn(async move { - tracker_cleanup_clone.clone().torrents_sharding.cleanup_threads(tracker_cleanup_clone.clone(), tokio_shutdown_cleanup_clone, Duration::from_secs(tracker_cleanup_clone.config.tracker_config.clone().peers_timeout), tracker_cleanup_clone.config.database.clone().persistent).await; + torrents_sharding.cleanup_threads( + tracker_cleanup_clone, + cleanup_handler, + Duration::from_secs(peers_timeout), + persistent + ).await; }); - if tracker.config.tracker_config.clone().keys_enabled { + if tracker_config.keys_enabled { let cleanup_keys_handler = tokio_shutdown.clone(); let tracker_spawn_cleanup_keys = tracker.clone(); - info!("[BOOT] Starting thread for keys cleanup with {} seconds delay...", tracker_spawn_cleanup_keys.config.tracker_config.clone().keys_cleanup_interval); + let keys_interval = tracker_spawn_cleanup_keys.config.tracker_config.keys_cleanup_interval; + info!("[BOOT] Starting thread for keys cleanup with {keys_interval} seconds delay..."); + tokio_core.spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(keys_interval)); loop { - tracker_spawn_cleanup_keys.set_stats(StatsEvent::TimestampKeysTimeout, chrono::Utc::now().timestamp() + tracker_spawn_cleanup_keys.config.tracker_config.clone().keys_cleanup_interval as i64); - if shutdown_waiting(Duration::from_secs(tracker_spawn_cleanup_keys.config.tracker_config.clone().keys_cleanup_interval), cleanup_keys_handler.clone()).await { - info!("[BOOT] Shutting down thread for keys cleanup..."); - return; + tokio::select! { + _ = interval.tick() => { + tracker_spawn_cleanup_keys.set_stats(StatsEvent::TimestampKeysTimeout, + chrono::Utc::now().timestamp() + keys_interval as i64); + info!("[KEYS] Checking now for outdated keys."); + tracker_spawn_cleanup_keys.clean_keys(); + info!("[KEYS] Keys cleaned up."); + } + _ = shutdown_waiting(Duration::from_secs(1), cleanup_keys_handler.clone()) => { + info!("[BOOT] Shutting down thread for keys cleanup..."); + return; + } } - - info!("[KEYS] Checking now for outdated keys."); - tracker_spawn_cleanup_keys.clean_keys(); - info!("[KEYS] Keys cleaned up."); } }); } - if tracker.config.database.clone().persistent { + if db_config.persistent { let updates_handler = tokio_shutdown.clone(); let tracker_spawn_updates = tracker.clone(); - info!("[BOOT] Starting thread for database updates with {} seconds delay...", tracker_spawn_updates.config.database.clone().persistent_interval); + let update_interval = tracker_spawn_updates.config.database.persistent_interval; + info!("[BOOT] Starting thread for database updates with {update_interval} seconds delay..."); + tokio_core.spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(update_interval)); loop { - tracker_spawn_updates.set_stats(StatsEvent::TimestampSave, chrono::Utc::now().timestamp() + tracker_spawn_updates.config.database.clone().persistent_interval as i64); - if shutdown_waiting(Duration::from_secs(tracker_spawn_updates.config.database.clone().persistent_interval), updates_handler.clone()).await { - info!("[BOOT] Shutting down thread for updates..."); - return; - } - - info!("[TORRENTS UPDATES] Start updating torrents into the DB."); - let _ = tracker_spawn_updates.save_torrent_updates(tracker_spawn_updates.clone()).await; - info!("[TORRENTS UPDATES] Torrent updates inserted into DB."); - - if tracker_spawn_updates.config.tracker_config.clone().whitelist_enabled { - info!("[WHITELIST UPDATES] Start updating whitelists into the DB."); - let _ = tracker_spawn_updates.save_whitelist_updates(tracker_spawn_updates.clone()).await; - info!("[WHITELIST UPDATES] Whitelists updates inserted into DB."); - } - - if tracker_spawn_updates.config.tracker_config.clone().blacklist_enabled { - info!("[BLACKLIST UPDATES] Start updating blacklists into the DB."); - let _ = tracker_spawn_updates.save_blacklist_updates(tracker_spawn_updates.clone()).await; - info!("[BLACKLIST UPDATES] Blacklists updates inserted into DB."); - } - - if tracker_spawn_updates.config.tracker_config.clone().keys_enabled { - info!("[KEY UPDATES] Start updating keys into the DB."); - let _ = tracker_spawn_updates.save_key_updates(tracker_spawn_updates.clone()).await; - info!("[KEY UPDATES] Keys updates inserted into DB."); - } - - if tracker_spawn_updates.config.tracker_config.clone().users_enabled { - info!("[USERS UPDATES] Start updating users into the DB."); - let _ = tracker_spawn_updates.save_user_updates(tracker_spawn_updates.clone()).await; - info!("[USERS UPDATES] Keys updates inserted into DB."); + tokio::select! { + _ = interval.tick() => { + tracker_spawn_updates.set_stats(StatsEvent::TimestampSave, + chrono::Utc::now().timestamp() + update_interval as i64); + + info!("[DATABASE UPDATES] Starting batch updates..."); + + let _ = tracker_spawn_updates.save_torrent_updates(tracker_spawn_updates.clone()).await; + + if tracker_spawn_updates.config.tracker_config.whitelist_enabled { + let _ = tracker_spawn_updates.save_whitelist_updates(tracker_spawn_updates.clone()).await; + } + if tracker_spawn_updates.config.tracker_config.blacklist_enabled { + let _ = tracker_spawn_updates.save_blacklist_updates(tracker_spawn_updates.clone()).await; + } + if tracker_spawn_updates.config.tracker_config.keys_enabled { + let _ = tracker_spawn_updates.save_key_updates(tracker_spawn_updates.clone()).await; + } + if tracker_spawn_updates.config.tracker_config.users_enabled { + let _ = tracker_spawn_updates.save_user_updates(tracker_spawn_updates.clone()).await; + } + + info!("[DATABASE UPDATES] Batch updates completed"); + } + _ = shutdown_waiting(Duration::from_secs(1), updates_handler.clone()) => { + info!("[BOOT] Shutting down thread for updates..."); + return; + } } } }); @@ -298,62 +377,52 @@ fn main() -> std::io::Result<()> tokio::select! { _ = tokio::signal::ctrl_c() => { info!("Shutdown request received, shutting down..."); - for handle in api_handlers.iter() { - handle.stop(true).await; - } - for handle in apis_handlers.iter() { - handle.stop(true).await; - } - for handle in http_handlers.iter() { - handle.stop(true).await; - } - for handle in https_handlers.iter() { - handle.stop(true).await; - } + let _ = udp_tx.send(true); - match udp_futures.into_iter().collect::>().await { + + match try_join_all(udp_futures).await { Ok(_) => {} Err(error) => { sentry::capture_error(&error); - error!("Errors happened on shutting down UDP sockets!"); - error!("{error}"); + error!("Errors happened on shutting down UDP sockets: {error}"); } } - tokio_shutdown.handle().await; + tokio_shutdown.handle().await; task::sleep(Duration::from_secs(1)).await; - if tracker.config.database.clone().persistent { - tracker.set_stats(StatsEvent::Completed, config.tracker_config.clone().total_downloads as i64); + if db_config.persistent { + tracker.set_stats(StatsEvent::Completed, config.tracker_config.total_downloads as i64); Configuration::save_from_config(tracker.config.clone(), "config.toml"); - info!("Saving completed data to an INI..."); - info!("Saving data to the database..."); + + info!("Saving final data to database..."); let _ = tracker.save_torrent_updates(tracker.clone()).await; - if tracker.config.tracker_config.clone().whitelist_enabled { + + if tracker_config.whitelist_enabled { let _ = tracker.save_whitelist_updates(tracker.clone()).await; } - if tracker.config.tracker_config.clone().blacklist_enabled { + if tracker_config.blacklist_enabled { let _ = tracker.save_blacklist_updates(tracker.clone()).await; } - if tracker.config.tracker_config.clone().keys_enabled { + if tracker_config.keys_enabled { let _ = tracker.save_key_updates(tracker.clone()).await; } - if tracker.config.tracker_config.clone().users_enabled { + if tracker_config.users_enabled { let _ = tracker.save_user_updates(tracker.clone()).await; } } else { - tracker.set_stats(StatsEvent::Completed, config.tracker_config.clone().total_downloads as i64); + tracker.set_stats(StatsEvent::Completed, config.tracker_config.total_downloads as i64); Configuration::save_from_config(tracker.config.clone(), "config.toml"); - info!("Saving completed data to an INI..."); + info!("Saving completed data to config..."); } task::sleep(Duration::from_secs(1)).await; - info!("Server shutting down completed"); + mem::forget(tokio_core); mem::forget(udp_tokio_threads); Ok(()) } } }) -} +} \ No newline at end of file diff --git a/src/stats/enums/stats_event.rs b/src/stats/enums/stats_event.rs index 58de54e..6d42eac 100644 --- a/src/stats/enums/stats_event.rs +++ b/src/stats/enums/stats_event.rs @@ -42,5 +42,6 @@ pub enum StatsEvent { Udp6InvalidRequest, Udp6ConnectionsHandled, Udp6AnnouncesHandled, - Udp6ScrapesHandled + Udp6ScrapesHandled, + UdpQueueLen } \ No newline at end of file diff --git a/src/stats/impls/torrent_tracker.rs b/src/stats/impls/torrent_tracker.rs index 1c67a81..8f89491 100644 --- a/src/stats/impls/torrent_tracker.rs +++ b/src/stats/impls/torrent_tracker.rs @@ -8,49 +8,50 @@ impl TorrentTracker { pub fn get_stats(&self) -> Stats { Stats { - started: self.stats.started.load(Ordering::SeqCst), - timestamp_run_save: self.stats.timestamp_run_save.load(Ordering::SeqCst), - timestamp_run_timeout: self.stats.timestamp_run_timeout.load(Ordering::SeqCst), - timestamp_run_console: self.stats.timestamp_run_console.load(Ordering::SeqCst), - timestamp_run_keys_timeout: self.stats.timestamp_run_keys_timeout.load(Ordering::SeqCst), - torrents: self.stats.torrents.load(Ordering::SeqCst), - torrents_updates: self.stats.torrents_updates.load(Ordering::SeqCst), - users: self.stats.users.load(Ordering::SeqCst), - users_updates: self.stats.users_updates.load(Ordering::SeqCst), - seeds: self.stats.seeds.load(Ordering::SeqCst), - peers: self.stats.peers.load(Ordering::SeqCst), - completed: self.stats.completed.load(Ordering::SeqCst), - whitelist_enabled: self.stats.whitelist_enabled.load(Ordering::SeqCst), - whitelist: self.stats.whitelist.load(Ordering::SeqCst), - whitelist_updates: self.stats.whitelist_updates.load(Ordering::SeqCst), - blacklist_enabled: self.stats.blacklist_enabled.load(Ordering::SeqCst), - blacklist: self.stats.blacklist.load(Ordering::SeqCst), - blacklist_updates: self.stats.blacklist_updates.load(Ordering::SeqCst), - keys_enabled: self.stats.keys_enabled.load(Ordering::SeqCst), - keys: self.stats.keys.load(Ordering::SeqCst), - keys_updates: self.stats.keys_updates.load(Ordering::SeqCst), - tcp4_not_found: self.stats.tcp4_not_found.load(Ordering::SeqCst), - tcp4_failure: self.stats.tcp4_failure.load(Ordering::SeqCst), - tcp4_connections_handled: self.stats.tcp4_connections_handled.load(Ordering::SeqCst), - tcp4_api_handled: self.stats.tcp4_api_handled.load(Ordering::SeqCst), - tcp4_announces_handled: self.stats.tcp4_announces_handled.load(Ordering::SeqCst), - tcp4_scrapes_handled: self.stats.tcp4_scrapes_handled.load(Ordering::SeqCst), - tcp6_not_found: self.stats.tcp6_not_found.load(Ordering::SeqCst), - tcp6_failure: self.stats.tcp6_failure.load(Ordering::SeqCst), - tcp6_connections_handled: self.stats.tcp6_connections_handled.load(Ordering::SeqCst), - tcp6_api_handled: self.stats.tcp6_api_handled.load(Ordering::SeqCst), - tcp6_announces_handled: self.stats.tcp6_announces_handled.load(Ordering::SeqCst), - tcp6_scrapes_handled: self.stats.tcp6_scrapes_handled.load(Ordering::SeqCst), - udp4_bad_request: self.stats.udp4_bad_request.load(Ordering::SeqCst), - udp4_invalid_request: self.stats.udp4_invalid_request.load(Ordering::SeqCst), - udp4_connections_handled: self.stats.udp4_connections_handled.load(Ordering::SeqCst), - udp4_announces_handled: self.stats.udp4_announces_handled.load(Ordering::SeqCst), - udp4_scrapes_handled: self.stats.udp4_scrapes_handled.load(Ordering::SeqCst), - udp6_bad_request: self.stats.udp6_bad_request.load(Ordering::SeqCst), - udp6_invalid_request: self.stats.udp6_invalid_request.load(Ordering::SeqCst), - udp6_connections_handled: self.stats.udp6_connections_handled.load(Ordering::SeqCst), - udp6_announces_handled: self.stats.udp6_announces_handled.load(Ordering::SeqCst), - udp6_scrapes_handled: self.stats.udp6_scrapes_handled.load(Ordering::SeqCst), + started: self.stats.started.load(Ordering::Relaxed), + timestamp_run_save: self.stats.timestamp_run_save.load(Ordering::Relaxed), + timestamp_run_timeout: self.stats.timestamp_run_timeout.load(Ordering::Relaxed), + timestamp_run_console: self.stats.timestamp_run_console.load(Ordering::Relaxed), + timestamp_run_keys_timeout: self.stats.timestamp_run_keys_timeout.load(Ordering::Relaxed), + torrents: self.stats.torrents.load(Ordering::Relaxed), + torrents_updates: self.stats.torrents_updates.load(Ordering::Relaxed), + users: self.stats.users.load(Ordering::Relaxed), + users_updates: self.stats.users_updates.load(Ordering::Relaxed), + seeds: self.stats.seeds.load(Ordering::Relaxed), + peers: self.stats.peers.load(Ordering::Relaxed), + completed: self.stats.completed.load(Ordering::Relaxed), + whitelist_enabled: self.stats.whitelist_enabled.load(Ordering::Relaxed), + whitelist: self.stats.whitelist.load(Ordering::Relaxed), + whitelist_updates: self.stats.whitelist_updates.load(Ordering::Relaxed), + blacklist_enabled: self.stats.blacklist_enabled.load(Ordering::Relaxed), + blacklist: self.stats.blacklist.load(Ordering::Relaxed), + blacklist_updates: self.stats.blacklist_updates.load(Ordering::Relaxed), + keys_enabled: self.stats.keys_enabled.load(Ordering::Relaxed), + keys: self.stats.keys.load(Ordering::Relaxed), + keys_updates: self.stats.keys_updates.load(Ordering::Relaxed), + tcp4_not_found: self.stats.tcp4_not_found.load(Ordering::Relaxed), + tcp4_failure: self.stats.tcp4_failure.load(Ordering::Relaxed), + tcp4_connections_handled: self.stats.tcp4_connections_handled.load(Ordering::Relaxed), + tcp4_api_handled: self.stats.tcp4_api_handled.load(Ordering::Relaxed), + tcp4_announces_handled: self.stats.tcp4_announces_handled.load(Ordering::Relaxed), + tcp4_scrapes_handled: self.stats.tcp4_scrapes_handled.load(Ordering::Relaxed), + tcp6_not_found: self.stats.tcp6_not_found.load(Ordering::Relaxed), + tcp6_failure: self.stats.tcp6_failure.load(Ordering::Relaxed), + tcp6_connections_handled: self.stats.tcp6_connections_handled.load(Ordering::Relaxed), + tcp6_api_handled: self.stats.tcp6_api_handled.load(Ordering::Relaxed), + tcp6_announces_handled: self.stats.tcp6_announces_handled.load(Ordering::Relaxed), + tcp6_scrapes_handled: self.stats.tcp6_scrapes_handled.load(Ordering::Relaxed), + udp4_bad_request: self.stats.udp4_bad_request.load(Ordering::Relaxed), + udp4_invalid_request: self.stats.udp4_invalid_request.load(Ordering::Relaxed), + udp4_connections_handled: self.stats.udp4_connections_handled.load(Ordering::Relaxed), + udp4_announces_handled: self.stats.udp4_announces_handled.load(Ordering::Relaxed), + udp4_scrapes_handled: self.stats.udp4_scrapes_handled.load(Ordering::Relaxed), + udp6_bad_request: self.stats.udp6_bad_request.load(Ordering::Relaxed), + udp6_invalid_request: self.stats.udp6_invalid_request.load(Ordering::Relaxed), + udp6_connections_handled: self.stats.udp6_connections_handled.load(Ordering::Relaxed), + udp6_announces_handled: self.stats.udp6_announces_handled.load(Ordering::Relaxed), + udp6_scrapes_handled: self.stats.udp6_scrapes_handled.load(Ordering::Relaxed), + udp_queue_len: self.stats.udp_queue_len.load(Ordering::Relaxed), } } @@ -59,168 +60,130 @@ impl TorrentTracker { { match event { StatsEvent::Torrents => { - if value > 0 { self.stats.torrents.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.torrents.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.torrents, value); } StatsEvent::TorrentsUpdates => { - if value > 0 { self.stats.torrents_updates.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.torrents_updates.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.torrents_updates, value); } StatsEvent::Users => { - if value > 0 { self.stats.users.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.users.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.users, value); } StatsEvent::UsersUpdates => { - if value > 0 { self.stats.users_updates.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.users_updates.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.users_updates, value); } StatsEvent::TimestampSave => { - if value > 0 { self.stats.timestamp_run_save.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.timestamp_run_save.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.timestamp_run_save, value); } StatsEvent::TimestampTimeout => { - if value > 0 { self.stats.timestamp_run_timeout.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.timestamp_run_timeout.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.timestamp_run_timeout, value); } StatsEvent::TimestampConsole => { - if value > 0 { self.stats.timestamp_run_console.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.timestamp_run_console.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.timestamp_run_console, value); } StatsEvent::TimestampKeysTimeout => { - if value > 0 { self.stats.timestamp_run_keys_timeout.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.timestamp_run_keys_timeout.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.timestamp_run_keys_timeout, value); } StatsEvent::Seeds => { - if value > 0 { self.stats.seeds.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.seeds.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.seeds, value); } StatsEvent::Peers => { - if value > 0 { self.stats.peers.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.peers.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.peers, value); } StatsEvent::Completed => { - if value > 0 { self.stats.completed.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.completed.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.completed, value); } StatsEvent::WhitelistEnabled => { - if value > 0 { self.stats.whitelist_enabled.store(true, Ordering::SeqCst); } - if value < 0 { self.stats.whitelist_enabled.store(false, Ordering::SeqCst); } + self.stats.whitelist_enabled.store(value > 0, Ordering::Release); } StatsEvent::Whitelist => { - if value > 0 { self.stats.whitelist.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.whitelist.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.whitelist, value); } StatsEvent::WhitelistUpdates => { - if value > 0 { self.stats.whitelist_updates.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.whitelist_updates.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.whitelist_updates, value); } StatsEvent::BlacklistEnabled => { - if value > 0 { self.stats.blacklist_enabled.store(true, Ordering::SeqCst); } - if value < 0 { self.stats.blacklist_enabled.store(false, Ordering::SeqCst); } + self.stats.blacklist_enabled.store(value > 0, Ordering::Release); } StatsEvent::Blacklist => { - if value > 0 { self.stats.blacklist.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.blacklist.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.blacklist, value); } StatsEvent::BlacklistUpdates => { - if value > 0 { self.stats.blacklist_updates.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.blacklist_updates.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.blacklist_updates, value); } StatsEvent::Key => { - if value > 0 { self.stats.keys.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.keys.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.keys, value); } StatsEvent::KeyUpdates => { - if value > 0 { self.stats.keys_updates.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.keys_updates.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.keys_updates, value); } StatsEvent::Tcp4NotFound => { - if value > 0 { self.stats.tcp4_not_found.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_not_found.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_not_found, value); } StatsEvent::Tcp4Failure => { - if value > 0 { self.stats.tcp4_failure.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_failure.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_failure, value); } StatsEvent::Tcp4ConnectionsHandled => { - if value > 0 { self.stats.tcp4_connections_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_connections_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_connections_handled, value); } StatsEvent::Tcp4ApiHandled => { - if value > 0 { self.stats.tcp4_api_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_api_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_api_handled, value); } StatsEvent::Tcp4AnnouncesHandled => { - if value > 0 { self.stats.tcp4_announces_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_announces_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_announces_handled, value); } StatsEvent::Tcp4ScrapesHandled => { - if value > 0 { self.stats.tcp4_scrapes_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp4_scrapes_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp4_scrapes_handled, value); } StatsEvent::Tcp6NotFound => { - if value > 0 { self.stats.tcp6_not_found.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_not_found.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_not_found, value); } StatsEvent::Tcp6Failure => { - if value > 0 { self.stats.tcp6_failure.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_failure.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_failure, value); } StatsEvent::Tcp6ConnectionsHandled => { - if value > 0 { self.stats.tcp6_connections_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_connections_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_connections_handled, value); } StatsEvent::Tcp6ApiHandled => { - if value > 0 { self.stats.tcp6_api_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_api_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_api_handled, value); } StatsEvent::Tcp6AnnouncesHandled => { - if value > 0 { self.stats.tcp6_announces_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_announces_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_announces_handled, value); } StatsEvent::Tcp6ScrapesHandled => { - if value > 0 { self.stats.tcp6_scrapes_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.tcp6_scrapes_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.tcp6_scrapes_handled, value); } StatsEvent::Udp4BadRequest => { - if value > 0 { self.stats.udp4_bad_request.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_bad_request.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp4_bad_request, value); } StatsEvent::Udp4InvalidRequest => { - if value > 0 { self.stats.udp4_invalid_request.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_invalid_request.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp4_invalid_request, value); } StatsEvent::Udp4ConnectionsHandled => { - if value > 0 { self.stats.udp4_connections_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_connections_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp4_connections_handled, value); } StatsEvent::Udp4AnnouncesHandled => { - if value > 0 { self.stats.udp4_announces_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_announces_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp4_announces_handled, value); } StatsEvent::Udp4ScrapesHandled => { - if value > 0 { self.stats.udp4_scrapes_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_scrapes_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp4_scrapes_handled, value); } StatsEvent::Udp6BadRequest => { - if value > 0 { self.stats.udp6_bad_request.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp6_bad_request.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp6_bad_request, value); } StatsEvent::Udp6InvalidRequest => { - if value > 0 { self.stats.udp4_invalid_request.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp4_invalid_request.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp6_invalid_request, value); } StatsEvent::Udp6ConnectionsHandled => { - if value > 0 { self.stats.udp6_connections_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp6_connections_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp6_connections_handled, value); } StatsEvent::Udp6AnnouncesHandled => { - if value > 0 { self.stats.udp6_announces_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp6_announces_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp6_announces_handled, value); } StatsEvent::Udp6ScrapesHandled => { - if value > 0 { self.stats.udp6_scrapes_handled.fetch_add(value, Ordering::SeqCst); } - if value < 0 { self.stats.udp6_scrapes_handled.fetch_sub(-value, Ordering::SeqCst); } + self.update_counter(&self.stats.udp6_scrapes_handled, value); + } + StatsEvent::UdpQueueLen => { + self.stats.udp_queue_len.store(value, Ordering::Release); } }; self.get_stats() @@ -231,131 +194,141 @@ impl TorrentTracker { { match event { StatsEvent::Torrents => { - self.stats.torrents.store(value, Ordering::SeqCst); + self.stats.torrents.store(value, Ordering::Release); } StatsEvent::TorrentsUpdates => { - self.stats.torrents_updates.store(value, Ordering::SeqCst); + self.stats.torrents_updates.store(value, Ordering::Release); } StatsEvent::Users => { - self.stats.users.store(value, Ordering::SeqCst); + self.stats.users.store(value, Ordering::Release); } StatsEvent::UsersUpdates => { - self.stats.users_updates.store(value, Ordering::SeqCst); + self.stats.users_updates.store(value, Ordering::Release); } StatsEvent::TimestampSave => { - self.stats.timestamp_run_save.store(value, Ordering::SeqCst); + self.stats.timestamp_run_save.store(value, Ordering::Release); } StatsEvent::TimestampTimeout => { - self.stats.timestamp_run_timeout.store(value, Ordering::SeqCst); + self.stats.timestamp_run_timeout.store(value, Ordering::Release); } StatsEvent::TimestampConsole => { - self.stats.timestamp_run_console.store(value, Ordering::SeqCst); + self.stats.timestamp_run_console.store(value, Ordering::Release); } StatsEvent::TimestampKeysTimeout => { - self.stats.timestamp_run_keys_timeout.store(value, Ordering::SeqCst); + self.stats.timestamp_run_keys_timeout.store(value, Ordering::Release); } StatsEvent::Seeds => { - self.stats.seeds.store(value, Ordering::SeqCst); + self.stats.seeds.store(value, Ordering::Release); } StatsEvent::Peers => { - self.stats.peers.store(value, Ordering::SeqCst); + self.stats.peers.store(value, Ordering::Release); } StatsEvent::Completed => { - self.stats.completed.store(value, Ordering::SeqCst); + self.stats.completed.store(value, Ordering::Release); } StatsEvent::WhitelistEnabled => { - if value > 0 { self.stats.whitelist_enabled.store(true, Ordering::SeqCst); } - if value < 0 { self.stats.whitelist_enabled.store(false, Ordering::SeqCst); } + self.stats.whitelist_enabled.store(value > 0, Ordering::Release); } StatsEvent::Whitelist => { - self.stats.whitelist.store(value, Ordering::SeqCst); + self.stats.whitelist.store(value, Ordering::Release); } StatsEvent::WhitelistUpdates => { - self.stats.whitelist_updates.store(value, Ordering::SeqCst); + self.stats.whitelist_updates.store(value, Ordering::Release); } StatsEvent::BlacklistEnabled => { - if value > 0 { self.stats.blacklist_enabled.store(true, Ordering::SeqCst); } - if value < 0 { self.stats.blacklist_enabled.store(false, Ordering::SeqCst); } + self.stats.blacklist_enabled.store(value > 0, Ordering::Release); } StatsEvent::Blacklist => { - self.stats.blacklist.store(value, Ordering::SeqCst); + self.stats.blacklist.store(value, Ordering::Release); } StatsEvent::BlacklistUpdates => { - self.stats.blacklist_updates.store(value, Ordering::SeqCst); + self.stats.blacklist_updates.store(value, Ordering::Release); } StatsEvent::Key => { - self.stats.keys.store(value, Ordering::SeqCst); + self.stats.keys.store(value, Ordering::Release); } StatsEvent::KeyUpdates => { - self.stats.keys_updates.store(value, Ordering::SeqCst); + self.stats.keys_updates.store(value, Ordering::Release); } StatsEvent::Tcp4NotFound => { - self.stats.tcp4_not_found.store(value, Ordering::SeqCst); + self.stats.tcp4_not_found.store(value, Ordering::Release); } StatsEvent::Tcp4Failure => { - self.stats.tcp4_failure.store(value, Ordering::SeqCst); + self.stats.tcp4_failure.store(value, Ordering::Release); } StatsEvent::Tcp4ConnectionsHandled => { - self.stats.tcp4_connections_handled.store(value, Ordering::SeqCst); + self.stats.tcp4_connections_handled.store(value, Ordering::Release); } StatsEvent::Tcp4ApiHandled => { - self.stats.tcp4_api_handled.store(value, Ordering::SeqCst); + self.stats.tcp4_api_handled.store(value, Ordering::Release); } StatsEvent::Tcp4AnnouncesHandled => { - self.stats.tcp4_announces_handled.store(value, Ordering::SeqCst); + self.stats.tcp4_announces_handled.store(value, Ordering::Release); } StatsEvent::Tcp4ScrapesHandled => { - self.stats.tcp4_scrapes_handled.store(value, Ordering::SeqCst); + self.stats.tcp4_scrapes_handled.store(value, Ordering::Release); } StatsEvent::Tcp6NotFound => { - self.stats.tcp6_not_found.store(value, Ordering::SeqCst); + self.stats.tcp6_not_found.store(value, Ordering::Release); } StatsEvent::Tcp6Failure => { - self.stats.tcp6_failure.store(value, Ordering::SeqCst); + self.stats.tcp6_failure.store(value, Ordering::Release); } StatsEvent::Tcp6ConnectionsHandled => { - self.stats.tcp6_connections_handled.store(value, Ordering::SeqCst); + self.stats.tcp6_connections_handled.store(value, Ordering::Release); } StatsEvent::Tcp6ApiHandled => { - self.stats.tcp6_api_handled.store(value, Ordering::SeqCst); + self.stats.tcp6_api_handled.store(value, Ordering::Release); } StatsEvent::Tcp6AnnouncesHandled => { - self.stats.tcp6_announces_handled.store(value, Ordering::SeqCst); + self.stats.tcp6_announces_handled.store(value, Ordering::Release); } StatsEvent::Tcp6ScrapesHandled => { - self.stats.tcp6_scrapes_handled.store(value, Ordering::SeqCst); + self.stats.tcp6_scrapes_handled.store(value, Ordering::Release); } StatsEvent::Udp4BadRequest => { - self.stats.udp4_bad_request.store(value, Ordering::SeqCst); + self.stats.udp4_bad_request.store(value, Ordering::Release); } StatsEvent::Udp4InvalidRequest => { - self.stats.udp4_bad_request.store(value, Ordering::SeqCst); + self.stats.udp4_invalid_request.store(value, Ordering::Release); } StatsEvent::Udp4ConnectionsHandled => { - self.stats.udp4_connections_handled.store(value, Ordering::SeqCst); + self.stats.udp4_connections_handled.store(value, Ordering::Release); } StatsEvent::Udp4AnnouncesHandled => { - self.stats.udp4_announces_handled.store(value, Ordering::SeqCst); + self.stats.udp4_announces_handled.store(value, Ordering::Release); } StatsEvent::Udp4ScrapesHandled => { - self.stats.udp4_scrapes_handled.store(value, Ordering::SeqCst); + self.stats.udp4_scrapes_handled.store(value, Ordering::Release); } StatsEvent::Udp6BadRequest => { - self.stats.udp6_bad_request.store(value, Ordering::SeqCst); + self.stats.udp6_bad_request.store(value, Ordering::Release); } StatsEvent::Udp6InvalidRequest => { - self.stats.udp6_bad_request.store(value, Ordering::SeqCst); + self.stats.udp6_invalid_request.store(value, Ordering::Release); } StatsEvent::Udp6ConnectionsHandled => { - self.stats.udp6_connections_handled.store(value, Ordering::SeqCst); + self.stats.udp6_connections_handled.store(value, Ordering::Release); } StatsEvent::Udp6AnnouncesHandled => { - self.stats.udp6_announces_handled.store(value, Ordering::SeqCst); + self.stats.udp6_announces_handled.store(value, Ordering::Release); } StatsEvent::Udp6ScrapesHandled => { - self.stats.udp6_scrapes_handled.store(value, Ordering::SeqCst); + self.stats.udp6_scrapes_handled.store(value, Ordering::Release); + } + StatsEvent::UdpQueueLen => { + self.stats.udp_queue_len.store(value, Ordering::Release); } }; self.get_stats() } + + #[inline(always)] + fn update_counter(&self, counter: &std::sync::atomic::AtomicI64, value: i64) { + if value > 0 { + counter.fetch_add(value, Ordering::Release); + } else if value < 0 { + counter.fetch_sub(-value, Ordering::Release); + } + } } \ No newline at end of file diff --git a/src/stats/structs/stats.rs b/src/stats/structs/stats.rs index 91873aa..8c03f07 100644 --- a/src/stats/structs/stats.rs +++ b/src/stats/structs/stats.rs @@ -45,4 +45,5 @@ pub struct Stats { pub udp6_connections_handled: i64, pub udp6_announces_handled: i64, pub udp6_scrapes_handled: i64, + pub udp_queue_len: i64, } \ No newline at end of file diff --git a/src/stats/structs/stats_atomics.rs b/src/stats/structs/stats_atomics.rs index 386059e..c9efb6a 100644 --- a/src/stats/structs/stats_atomics.rs +++ b/src/stats/structs/stats_atomics.rs @@ -46,4 +46,5 @@ pub struct StatsAtomics { pub udp6_connections_handled: AtomicI64, pub udp6_announces_handled: AtomicI64, pub udp6_scrapes_handled: AtomicI64, + pub udp_queue_len: AtomicI64, } \ No newline at end of file diff --git a/src/tracker/impls.rs b/src/tracker/impls.rs index 6186077..5ad1e6d 100644 --- a/src/tracker/impls.rs +++ b/src/tracker/impls.rs @@ -1,7 +1,5 @@ pub mod info_hash; -pub mod info_hash_visitor; pub mod peer_id; -pub mod peer_id_visitor; pub mod torrent_entry; pub mod torrent_peer; pub mod torrent_tracker; @@ -15,7 +13,6 @@ pub mod torrent_tracker_torrents_whitelist; pub mod torrent_tracker_users; pub mod torrent_tracker_users_updates; pub mod user_id; -pub mod user_id_visitor; pub mod announce_event; pub mod torrent_sharding; pub mod torrent_tracker_import; @@ -23,4 +20,5 @@ pub mod torrent_tracker_export; pub mod torrent_tracker_cert_gen; pub mod torrent_tracker_torrents_blacklist_updates; pub mod torrent_tracker_torrents_whitelist_updates; -pub mod torrent_tracker_keys_updates; \ No newline at end of file +pub mod torrent_tracker_keys_updates; +pub mod cleanup_stats_atomics; \ No newline at end of file diff --git a/src/tracker/impls/announce_event.rs b/src/tracker/impls/announce_event.rs index 0a3c077..e2b3200 100644 --- a/src/tracker/impls/announce_event.rs +++ b/src/tracker/impls/announce_event.rs @@ -2,7 +2,7 @@ use crate::tracker::enums::announce_event::AnnounceEvent; impl AnnounceEvent { #[inline] - pub fn from_i32(i: i32) -> Self { + pub const fn from_i32(i: i32) -> Self { match i { 1 => Self::Completed, 2 => Self::Started, @@ -12,7 +12,7 @@ impl AnnounceEvent { } #[inline] - pub fn to_i32(&self) -> i32 { + pub const fn to_i32(self) -> i32 { match self { AnnounceEvent::None => 0, AnnounceEvent::Completed => 1, diff --git a/src/tracker/impls/cleanup_stats_atomics.rs b/src/tracker/impls/cleanup_stats_atomics.rs new file mode 100644 index 0000000..22d6456 --- /dev/null +++ b/src/tracker/impls/cleanup_stats_atomics.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use log::info; +use crate::tracker::impls::torrent_sharding::CACHE_LINE_SIZE; +use crate::tracker::structs::cleanup_stats_atomic::CleanupStatsAtomic; +use crate::tracker::structs::padded_atomic_u64::PaddedAtomicU64; +use crate::tracker::structs::torrent_tracker::TorrentTracker; + +impl CleanupStatsAtomic { + pub(crate) fn new() -> Self { + Self { + torrents: PaddedAtomicU64 { + value: AtomicU64::new(0), + _padding: [0; CACHE_LINE_SIZE - std::mem::size_of::()], + }, + seeds: PaddedAtomicU64 { + value: AtomicU64::new(0), + _padding: [0; CACHE_LINE_SIZE - std::mem::size_of::()], + }, + peers: PaddedAtomicU64 { + value: AtomicU64::new(0), + _padding: [0; CACHE_LINE_SIZE - std::mem::size_of::()], + }, + } + } + + pub(crate) fn add_torrents(&self, count: u64) { + self.torrents.value.fetch_add(count, Ordering::Relaxed); + } + + pub(crate) fn add_seeds(&self, count: u64) { + self.seeds.value.fetch_add(count, Ordering::Relaxed); + } + + pub(crate) fn add_peers(&self, count: u64) { + self.peers.value.fetch_add(count, Ordering::Relaxed); + } + + pub(crate) fn apply_to_tracker(&self, _tracker: &Arc) { + let torrents = self.torrents.value.load(Ordering::Relaxed); + let seeds = self.seeds.value.load(Ordering::Relaxed); + let peers = self.peers.value.load(Ordering::Relaxed); + + if torrents > 0 || seeds > 0 || peers > 0 { + info!("[CLEANUP TOTAL] Torrents: {torrents} - Seeds: {seeds} - Peers: {peers}"); + } + } +} \ No newline at end of file diff --git a/src/tracker/impls/info_hash.rs b/src/tracker/impls/info_hash.rs index 22bd797..bcd3a98 100644 --- a/src/tracker/impls/info_hash.rs +++ b/src/tracker/impls/info_hash.rs @@ -2,7 +2,6 @@ use std::fmt; use std::fmt::Formatter; use crate::common::common::bin2hex; use crate::tracker::structs::info_hash::InfoHash; -use crate::tracker::structs::info_hash_visitor::InfoHashVisitor; impl fmt::Display for InfoHash { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -14,12 +13,25 @@ impl std::str::FromStr for InfoHash { type Err = binascii::ConvertError; fn from_str(s: &str) -> Result { - let mut i = Self([0u8; 20]); if s.len() != 40 { return Err(binascii::ConvertError::InvalidInputLength); } - binascii::hex2bin(s.as_bytes(), &mut i.0)?; - Ok(i) + + let mut result = InfoHash([0u8; 20]); + let bytes = s.as_bytes(); + + for (i, chunk) in bytes.chunks_exact(2).enumerate() { + let high = hex_to_nibble(chunk[0]); + let low = hex_to_nibble(chunk[1]); + + if high == 0xFF || low == 0xFF { + return Err(binascii::ConvertError::InvalidInput); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) } } @@ -27,7 +39,7 @@ impl From<&[u8]> for InfoHash { fn from(data: &[u8]) -> InfoHash { assert_eq!(data.len(), 20); let mut ret = InfoHash([0u8; 20]); - ret.0.clone_from_slice(data); + ret.0.copy_from_slice(data); ret } } @@ -40,15 +52,92 @@ impl From<[u8; 20]> for InfoHash { impl serde::ser::Serialize for InfoHash { fn serialize(&self, serializer: S) -> Result { + const HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; let mut buffer = [0u8; 40]; - let bytes_out = binascii::bin2hex(&self.0, &mut buffer).ok().unwrap(); - let str_out = std::str::from_utf8(bytes_out).unwrap(); + + for (i, &byte) in self.0.iter().enumerate() { + let idx = i * 2; + buffer[idx] = HEX_CHARS[(byte >> 4) as usize]; + buffer[idx + 1] = HEX_CHARS[(byte & 0xf) as usize]; + } + + // SAFETY: We know the buffer contains only valid ASCII hex characters + let str_out = unsafe { std::str::from_utf8_unchecked(&buffer) }; serializer.serialize_str(str_out) } } impl<'de> serde::de::Deserialize<'de> for InfoHash { fn deserialize>(des: D) -> Result { + struct InfoHashVisitor; + + impl<'de> serde::de::Visitor<'de> for InfoHashVisitor { + type Value = InfoHash; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a 40 character hex string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() != 40 { + return Err(E::custom("expected 40 character hex string")); + } + + let mut result = InfoHash([0u8; 20]); + let bytes = v.as_bytes(); + + for (i, chunk) in bytes.chunks_exact(2).enumerate() { + let high = hex_to_nibble(chunk[0]); + let low = hex_to_nibble(chunk[1]); + + if high == 0xFF || low == 0xFF { + return Err(E::custom("invalid hex character")); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if v.len() != 40 { + return Err(E::custom("expected 40 byte hex string")); + } + + let mut result = InfoHash([0u8; 20]); + + for (i, chunk) in v.chunks_exact(2).enumerate() { + let high = hex_to_nibble(chunk[0]); + let low = hex_to_nibble(chunk[1]); + + if high == 0xFF || low == 0xFF { + return Err(E::custom("invalid hex character")); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) + } + } + des.deserialize_str(InfoHashVisitor) } +} + +#[inline(always)] +fn hex_to_nibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => 0xFF, + } } \ No newline at end of file diff --git a/src/tracker/impls/info_hash_visitor.rs b/src/tracker/impls/info_hash_visitor.rs deleted file mode 100644 index 56fafc2..0000000 --- a/src/tracker/impls/info_hash_visitor.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::fmt; -use std::fmt::Formatter; -use crate::tracker::structs::info_hash::InfoHash; -use crate::tracker::structs::info_hash_visitor::InfoHashVisitor; - -impl serde::de::Visitor<'_> for InfoHashVisitor { - type Value = InfoHash; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - write!(formatter, "a 40 character long hash") - } - - fn visit_str(self, v: &str) -> Result { - if v.len() != 40 { - return Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a 40 character long string", - )); - } - - let mut res = InfoHash([0u8; 20]); - - if binascii::hex2bin(v.as_bytes(), &mut res.0).is_err() { - Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a hexadecimal string", - )) - } else { - Ok(res) - } - } -} \ No newline at end of file diff --git a/src/tracker/impls/peer_id.rs b/src/tracker/impls/peer_id.rs index d9c4f5d..00f10e4 100644 --- a/src/tracker/impls/peer_id.rs +++ b/src/tracker/impls/peer_id.rs @@ -3,7 +3,6 @@ use std::fmt::Formatter; use serde::Serialize; use crate::common::common::bin2hex; use crate::tracker::structs::peer_id::PeerId; -use crate::tracker::structs::peer_id_visitor::PeerIdVisitor; impl fmt::Display for PeerId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -94,16 +93,23 @@ impl PeerId { impl Serialize for PeerId { fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, { - let buff_size = self.0.len() * 2; - let mut tmp: Vec = vec![0; buff_size]; - binascii::bin2hex(&self.0, &mut tmp).unwrap(); - let id = std::str::from_utf8(&tmp).ok(); + where + S: serde::Serializer, + { + const HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; + let mut buffer = [0u8; 40]; + + for (i, &byte) in self.0.iter().enumerate() { + buffer[i * 2] = HEX_CHARS[(byte >> 4) as usize]; + buffer[i * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize]; + } + + // SAFETY: We know the buffer contains only valid ASCII hex characters + let id = unsafe { std::str::from_utf8_unchecked(&buffer) }; #[derive(Serialize)] struct PeerIdInfo<'a> { - id: Option<&'a str>, + id: &'a str, client: Option<&'a str>, } @@ -119,12 +125,25 @@ impl std::str::FromStr for PeerId { type Err = binascii::ConvertError; fn from_str(s: &str) -> Result { - let mut i = Self([0u8; 20]); if s.len() != 40 { return Err(binascii::ConvertError::InvalidInputLength); } - binascii::hex2bin(s.as_bytes(), &mut i.0)?; - Ok(i) + + let mut result = PeerId([0u8; 20]); + let bytes = s.as_bytes(); + + for i in 0..20 { + let high = hex_char_to_nibble(bytes[i * 2]); + let low = hex_char_to_nibble(bytes[i * 2 + 1]); + + if high == 0xFF || low == 0xFF { + return Err(binascii::ConvertError::InvalidInput); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) } } @@ -132,13 +151,64 @@ impl From<&[u8]> for PeerId { fn from(data: &[u8]) -> PeerId { assert_eq!(data.len(), 20); let mut ret = PeerId([0u8; 20]); - ret.0.clone_from_slice(data); + ret.0.copy_from_slice(data); ret } } impl<'de> serde::de::Deserialize<'de> for PeerId { fn deserialize>(des: D) -> Result { + struct PeerIdVisitor; + + impl<'de> serde::de::Visitor<'de> for PeerIdVisitor { + type Value = PeerId; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a 40 character long hash") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() != 40 { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"expected a 40 character long string", + )); + } + + let mut res = PeerId([0u8; 20]); + let bytes = v.as_bytes(); + + for i in 0..20 { + let high = hex_char_to_nibble(bytes[i * 2]); + let low = hex_char_to_nibble(bytes[i * 2 + 1]); + + if high == 0xFF || low == 0xFF { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"expected a hexadecimal string", + )); + } + + res.0[i] = (high << 4) | low; + } + + Ok(res) + } + } + des.deserialize_str(PeerIdVisitor) } +} + +#[inline(always)] +fn hex_char_to_nibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => 0xFF, + } } \ No newline at end of file diff --git a/src/tracker/impls/peer_id_visitor.rs b/src/tracker/impls/peer_id_visitor.rs deleted file mode 100644 index a0e375c..0000000 --- a/src/tracker/impls/peer_id_visitor.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::fmt; -use std::fmt::Formatter; -use crate::tracker::structs::peer_id::PeerId; -use crate::tracker::structs::peer_id_visitor::PeerIdVisitor; - -impl serde::de::Visitor<'_> for PeerIdVisitor { - type Value = PeerId; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - write!(formatter, "a 40 character long hash") - } - - fn visit_str(self, v: &str) -> Result { - if v.len() != 40 { - return Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a 40 character long string", - )); - } - - let mut res = PeerId([0u8; 20]); - - if binascii::hex2bin(v.as_bytes(), &mut res.0).is_err() { - Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a hexadecimal string", - )) - } else { - Ok(res) - } - } -} \ No newline at end of file diff --git a/src/tracker/impls/torrent_sharding.rs b/src/tracker/impls/torrent_sharding.rs index ee69ebf..e6e5c3a 100644 --- a/src/tracker/impls/torrent_sharding.rs +++ b/src/tracker/impls/torrent_sharding.rs @@ -1,677 +1,432 @@ use std::collections::btree_map::Entry; use std::collections::BTreeMap; -use std::mem; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use log::info; use parking_lot::RwLock; use tokio::runtime::Builder; +use tokio::sync::Semaphore; +use tokio::task::JoinHandle; use tokio_shutdown::Shutdown; use crate::common::common::shutdown_waiting; -use crate::stats::enums::stats_event::StatsEvent; +use crate::tracker::structs::cleanup_stats_atomic::CleanupStatsAtomic; use crate::tracker::structs::info_hash::InfoHash; use crate::tracker::structs::peer_id::PeerId; use crate::tracker::structs::torrent_entry::TorrentEntry; use crate::tracker::structs::torrent_sharding::TorrentSharding; use crate::tracker::structs::torrent_tracker::TorrentTracker; -#[allow(dead_code)] -impl TorrentSharding { +pub const CACHE_LINE_SIZE: usize = 64; + +impl Default for TorrentSharding { fn default() -> Self { Self::new() } +} +#[allow(dead_code)] +impl TorrentSharding { #[tracing::instrument(level = "debug")] - pub fn new() -> TorrentSharding - { + pub fn new() -> TorrentSharding { TorrentSharding { - shard_000: Arc::new(RwLock::new(Default::default())), - shard_001: Arc::new(RwLock::new(Default::default())), - shard_002: Arc::new(RwLock::new(Default::default())), - shard_003: Arc::new(RwLock::new(Default::default())), - shard_004: Arc::new(RwLock::new(Default::default())), - shard_005: Arc::new(RwLock::new(Default::default())), - shard_006: Arc::new(RwLock::new(Default::default())), - shard_007: Arc::new(RwLock::new(Default::default())), - shard_008: Arc::new(RwLock::new(Default::default())), - shard_009: Arc::new(RwLock::new(Default::default())), - shard_010: Arc::new(RwLock::new(Default::default())), - shard_011: Arc::new(RwLock::new(Default::default())), - shard_012: Arc::new(RwLock::new(Default::default())), - shard_013: Arc::new(RwLock::new(Default::default())), - shard_014: Arc::new(RwLock::new(Default::default())), - shard_015: Arc::new(RwLock::new(Default::default())), - shard_016: Arc::new(RwLock::new(Default::default())), - shard_017: Arc::new(RwLock::new(Default::default())), - shard_018: Arc::new(RwLock::new(Default::default())), - shard_019: Arc::new(RwLock::new(Default::default())), - shard_020: Arc::new(RwLock::new(Default::default())), - shard_021: Arc::new(RwLock::new(Default::default())), - shard_022: Arc::new(RwLock::new(Default::default())), - shard_023: Arc::new(RwLock::new(Default::default())), - shard_024: Arc::new(RwLock::new(Default::default())), - shard_025: Arc::new(RwLock::new(Default::default())), - shard_026: Arc::new(RwLock::new(Default::default())), - shard_027: Arc::new(RwLock::new(Default::default())), - shard_028: Arc::new(RwLock::new(Default::default())), - shard_029: Arc::new(RwLock::new(Default::default())), - shard_030: Arc::new(RwLock::new(Default::default())), - shard_031: Arc::new(RwLock::new(Default::default())), - shard_032: Arc::new(RwLock::new(Default::default())), - shard_033: Arc::new(RwLock::new(Default::default())), - shard_034: Arc::new(RwLock::new(Default::default())), - shard_035: Arc::new(RwLock::new(Default::default())), - shard_036: Arc::new(RwLock::new(Default::default())), - shard_037: Arc::new(RwLock::new(Default::default())), - shard_038: Arc::new(RwLock::new(Default::default())), - shard_039: Arc::new(RwLock::new(Default::default())), - shard_040: Arc::new(RwLock::new(Default::default())), - shard_041: Arc::new(RwLock::new(Default::default())), - shard_042: Arc::new(RwLock::new(Default::default())), - shard_043: Arc::new(RwLock::new(Default::default())), - shard_044: Arc::new(RwLock::new(Default::default())), - shard_045: Arc::new(RwLock::new(Default::default())), - shard_046: Arc::new(RwLock::new(Default::default())), - shard_047: Arc::new(RwLock::new(Default::default())), - shard_048: Arc::new(RwLock::new(Default::default())), - shard_049: Arc::new(RwLock::new(Default::default())), - shard_050: Arc::new(RwLock::new(Default::default())), - shard_051: Arc::new(RwLock::new(Default::default())), - shard_052: Arc::new(RwLock::new(Default::default())), - shard_053: Arc::new(RwLock::new(Default::default())), - shard_054: Arc::new(RwLock::new(Default::default())), - shard_055: Arc::new(RwLock::new(Default::default())), - shard_056: Arc::new(RwLock::new(Default::default())), - shard_057: Arc::new(RwLock::new(Default::default())), - shard_058: Arc::new(RwLock::new(Default::default())), - shard_059: Arc::new(RwLock::new(Default::default())), - shard_060: Arc::new(RwLock::new(Default::default())), - shard_061: Arc::new(RwLock::new(Default::default())), - shard_062: Arc::new(RwLock::new(Default::default())), - shard_063: Arc::new(RwLock::new(Default::default())), - shard_064: Arc::new(RwLock::new(Default::default())), - shard_065: Arc::new(RwLock::new(Default::default())), - shard_066: Arc::new(RwLock::new(Default::default())), - shard_067: Arc::new(RwLock::new(Default::default())), - shard_068: Arc::new(RwLock::new(Default::default())), - shard_069: Arc::new(RwLock::new(Default::default())), - shard_070: Arc::new(RwLock::new(Default::default())), - shard_071: Arc::new(RwLock::new(Default::default())), - shard_072: Arc::new(RwLock::new(Default::default())), - shard_073: Arc::new(RwLock::new(Default::default())), - shard_074: Arc::new(RwLock::new(Default::default())), - shard_075: Arc::new(RwLock::new(Default::default())), - shard_076: Arc::new(RwLock::new(Default::default())), - shard_077: Arc::new(RwLock::new(Default::default())), - shard_078: Arc::new(RwLock::new(Default::default())), - shard_079: Arc::new(RwLock::new(Default::default())), - shard_080: Arc::new(RwLock::new(Default::default())), - shard_081: Arc::new(RwLock::new(Default::default())), - shard_082: Arc::new(RwLock::new(Default::default())), - shard_083: Arc::new(RwLock::new(Default::default())), - shard_084: Arc::new(RwLock::new(Default::default())), - shard_085: Arc::new(RwLock::new(Default::default())), - shard_086: Arc::new(RwLock::new(Default::default())), - shard_087: Arc::new(RwLock::new(Default::default())), - shard_088: Arc::new(RwLock::new(Default::default())), - shard_089: Arc::new(RwLock::new(Default::default())), - shard_090: Arc::new(RwLock::new(Default::default())), - shard_091: Arc::new(RwLock::new(Default::default())), - shard_092: Arc::new(RwLock::new(Default::default())), - shard_093: Arc::new(RwLock::new(Default::default())), - shard_094: Arc::new(RwLock::new(Default::default())), - shard_095: Arc::new(RwLock::new(Default::default())), - shard_096: Arc::new(RwLock::new(Default::default())), - shard_097: Arc::new(RwLock::new(Default::default())), - shard_098: Arc::new(RwLock::new(Default::default())), - shard_099: Arc::new(RwLock::new(Default::default())), - shard_100: Arc::new(RwLock::new(Default::default())), - shard_101: Arc::new(RwLock::new(Default::default())), - shard_102: Arc::new(RwLock::new(Default::default())), - shard_103: Arc::new(RwLock::new(Default::default())), - shard_104: Arc::new(RwLock::new(Default::default())), - shard_105: Arc::new(RwLock::new(Default::default())), - shard_106: Arc::new(RwLock::new(Default::default())), - shard_107: Arc::new(RwLock::new(Default::default())), - shard_108: Arc::new(RwLock::new(Default::default())), - shard_109: Arc::new(RwLock::new(Default::default())), - shard_110: Arc::new(RwLock::new(Default::default())), - shard_111: Arc::new(RwLock::new(Default::default())), - shard_112: Arc::new(RwLock::new(Default::default())), - shard_113: Arc::new(RwLock::new(Default::default())), - shard_114: Arc::new(RwLock::new(Default::default())), - shard_115: Arc::new(RwLock::new(Default::default())), - shard_116: Arc::new(RwLock::new(Default::default())), - shard_117: Arc::new(RwLock::new(Default::default())), - shard_118: Arc::new(RwLock::new(Default::default())), - shard_119: Arc::new(RwLock::new(Default::default())), - shard_120: Arc::new(RwLock::new(Default::default())), - shard_121: Arc::new(RwLock::new(Default::default())), - shard_122: Arc::new(RwLock::new(Default::default())), - shard_123: Arc::new(RwLock::new(Default::default())), - shard_124: Arc::new(RwLock::new(Default::default())), - shard_125: Arc::new(RwLock::new(Default::default())), - shard_126: Arc::new(RwLock::new(Default::default())), - shard_127: Arc::new(RwLock::new(Default::default())), - shard_128: Arc::new(RwLock::new(Default::default())), - shard_129: Arc::new(RwLock::new(Default::default())), - shard_130: Arc::new(RwLock::new(Default::default())), - shard_131: Arc::new(RwLock::new(Default::default())), - shard_132: Arc::new(RwLock::new(Default::default())), - shard_133: Arc::new(RwLock::new(Default::default())), - shard_134: Arc::new(RwLock::new(Default::default())), - shard_135: Arc::new(RwLock::new(Default::default())), - shard_136: Arc::new(RwLock::new(Default::default())), - shard_137: Arc::new(RwLock::new(Default::default())), - shard_138: Arc::new(RwLock::new(Default::default())), - shard_139: Arc::new(RwLock::new(Default::default())), - shard_140: Arc::new(RwLock::new(Default::default())), - shard_141: Arc::new(RwLock::new(Default::default())), - shard_142: Arc::new(RwLock::new(Default::default())), - shard_143: Arc::new(RwLock::new(Default::default())), - shard_144: Arc::new(RwLock::new(Default::default())), - shard_145: Arc::new(RwLock::new(Default::default())), - shard_146: Arc::new(RwLock::new(Default::default())), - shard_147: Arc::new(RwLock::new(Default::default())), - shard_148: Arc::new(RwLock::new(Default::default())), - shard_149: Arc::new(RwLock::new(Default::default())), - shard_150: Arc::new(RwLock::new(Default::default())), - shard_151: Arc::new(RwLock::new(Default::default())), - shard_152: Arc::new(RwLock::new(Default::default())), - shard_153: Arc::new(RwLock::new(Default::default())), - shard_154: Arc::new(RwLock::new(Default::default())), - shard_155: Arc::new(RwLock::new(Default::default())), - shard_156: Arc::new(RwLock::new(Default::default())), - shard_157: Arc::new(RwLock::new(Default::default())), - shard_158: Arc::new(RwLock::new(Default::default())), - shard_159: Arc::new(RwLock::new(Default::default())), - shard_160: Arc::new(RwLock::new(Default::default())), - shard_161: Arc::new(RwLock::new(Default::default())), - shard_162: Arc::new(RwLock::new(Default::default())), - shard_163: Arc::new(RwLock::new(Default::default())), - shard_164: Arc::new(RwLock::new(Default::default())), - shard_165: Arc::new(RwLock::new(Default::default())), - shard_166: Arc::new(RwLock::new(Default::default())), - shard_167: Arc::new(RwLock::new(Default::default())), - shard_168: Arc::new(RwLock::new(Default::default())), - shard_169: Arc::new(RwLock::new(Default::default())), - shard_170: Arc::new(RwLock::new(Default::default())), - shard_171: Arc::new(RwLock::new(Default::default())), - shard_172: Arc::new(RwLock::new(Default::default())), - shard_173: Arc::new(RwLock::new(Default::default())), - shard_174: Arc::new(RwLock::new(Default::default())), - shard_175: Arc::new(RwLock::new(Default::default())), - shard_176: Arc::new(RwLock::new(Default::default())), - shard_177: Arc::new(RwLock::new(Default::default())), - shard_178: Arc::new(RwLock::new(Default::default())), - shard_179: Arc::new(RwLock::new(Default::default())), - shard_180: Arc::new(RwLock::new(Default::default())), - shard_181: Arc::new(RwLock::new(Default::default())), - shard_182: Arc::new(RwLock::new(Default::default())), - shard_183: Arc::new(RwLock::new(Default::default())), - shard_184: Arc::new(RwLock::new(Default::default())), - shard_185: Arc::new(RwLock::new(Default::default())), - shard_186: Arc::new(RwLock::new(Default::default())), - shard_187: Arc::new(RwLock::new(Default::default())), - shard_188: Arc::new(RwLock::new(Default::default())), - shard_189: Arc::new(RwLock::new(Default::default())), - shard_190: Arc::new(RwLock::new(Default::default())), - shard_191: Arc::new(RwLock::new(Default::default())), - shard_192: Arc::new(RwLock::new(Default::default())), - shard_193: Arc::new(RwLock::new(Default::default())), - shard_194: Arc::new(RwLock::new(Default::default())), - shard_195: Arc::new(RwLock::new(Default::default())), - shard_196: Arc::new(RwLock::new(Default::default())), - shard_197: Arc::new(RwLock::new(Default::default())), - shard_198: Arc::new(RwLock::new(Default::default())), - shard_199: Arc::new(RwLock::new(Default::default())), - shard_200: Arc::new(RwLock::new(Default::default())), - shard_201: Arc::new(RwLock::new(Default::default())), - shard_202: Arc::new(RwLock::new(Default::default())), - shard_203: Arc::new(RwLock::new(Default::default())), - shard_204: Arc::new(RwLock::new(Default::default())), - shard_205: Arc::new(RwLock::new(Default::default())), - shard_206: Arc::new(RwLock::new(Default::default())), - shard_207: Arc::new(RwLock::new(Default::default())), - shard_208: Arc::new(RwLock::new(Default::default())), - shard_209: Arc::new(RwLock::new(Default::default())), - shard_210: Arc::new(RwLock::new(Default::default())), - shard_211: Arc::new(RwLock::new(Default::default())), - shard_212: Arc::new(RwLock::new(Default::default())), - shard_213: Arc::new(RwLock::new(Default::default())), - shard_214: Arc::new(RwLock::new(Default::default())), - shard_215: Arc::new(RwLock::new(Default::default())), - shard_216: Arc::new(RwLock::new(Default::default())), - shard_217: Arc::new(RwLock::new(Default::default())), - shard_218: Arc::new(RwLock::new(Default::default())), - shard_219: Arc::new(RwLock::new(Default::default())), - shard_220: Arc::new(RwLock::new(Default::default())), - shard_221: Arc::new(RwLock::new(Default::default())), - shard_222: Arc::new(RwLock::new(Default::default())), - shard_223: Arc::new(RwLock::new(Default::default())), - shard_224: Arc::new(RwLock::new(Default::default())), - shard_225: Arc::new(RwLock::new(Default::default())), - shard_226: Arc::new(RwLock::new(Default::default())), - shard_227: Arc::new(RwLock::new(Default::default())), - shard_228: Arc::new(RwLock::new(Default::default())), - shard_229: Arc::new(RwLock::new(Default::default())), - shard_230: Arc::new(RwLock::new(Default::default())), - shard_231: Arc::new(RwLock::new(Default::default())), - shard_232: Arc::new(RwLock::new(Default::default())), - shard_233: Arc::new(RwLock::new(Default::default())), - shard_234: Arc::new(RwLock::new(Default::default())), - shard_235: Arc::new(RwLock::new(Default::default())), - shard_236: Arc::new(RwLock::new(Default::default())), - shard_237: Arc::new(RwLock::new(Default::default())), - shard_238: Arc::new(RwLock::new(Default::default())), - shard_239: Arc::new(RwLock::new(Default::default())), - shard_240: Arc::new(RwLock::new(Default::default())), - shard_241: Arc::new(RwLock::new(Default::default())), - shard_242: Arc::new(RwLock::new(Default::default())), - shard_243: Arc::new(RwLock::new(Default::default())), - shard_244: Arc::new(RwLock::new(Default::default())), - shard_245: Arc::new(RwLock::new(Default::default())), - shard_246: Arc::new(RwLock::new(Default::default())), - shard_247: Arc::new(RwLock::new(Default::default())), - shard_248: Arc::new(RwLock::new(Default::default())), - shard_249: Arc::new(RwLock::new(Default::default())), - shard_250: Arc::new(RwLock::new(Default::default())), - shard_251: Arc::new(RwLock::new(Default::default())), - shard_252: Arc::new(RwLock::new(Default::default())), - shard_253: Arc::new(RwLock::new(Default::default())), - shard_254: Arc::new(RwLock::new(Default::default())), - shard_255: Arc::new(RwLock::new(Default::default())), + shards: std::array::from_fn(|_| Arc::new(RwLock::new(BTreeMap::new()))), } } - pub async fn cleanup_threads(&self, torrent_tracker: Arc, shutdown: Shutdown, peer_timeout: Duration, persistent: bool) - { - let tokio_threading = match torrent_tracker.clone().config.tracker_config.peers_cleanup_threads { - 0 => { - Builder::new_current_thread().thread_name("sharding").enable_all().build().unwrap() - } - _ => { - Builder::new_multi_thread().thread_name("sharding").worker_threads(torrent_tracker.clone().config.tracker_config.peers_cleanup_threads as usize).enable_all().build().unwrap() - } - }; - for shard in 0u8..=255u8 { - let torrent_tracker_clone = torrent_tracker.clone(); + pub async fn cleanup_threads(&self, torrent_tracker: Arc, shutdown: Shutdown, peer_timeout: Duration, persistent: bool) { + let cleanup_interval = torrent_tracker.config.tracker_config.peers_cleanup_interval; + let cleanup_threads = torrent_tracker.config.tracker_config.peers_cleanup_threads; + + let cleanup_pool = Builder::new_multi_thread() + .worker_threads(cleanup_threads as usize) + .thread_name("cleanup-worker") + .enable_all() + .build() + .unwrap(); + + let max_concurrent = std::cmp::max(cleanup_threads as usize * 2, 8); + let semaphore = Arc::new(Semaphore::new(max_concurrent)); + + let cleanup_handles_capacity = 256; + + let timer_handle: JoinHandle<()> = cleanup_pool.spawn({ + let torrent_tracker_clone = Arc::clone(&torrent_tracker); let shutdown_clone = shutdown.clone(); - tokio_threading.spawn(async move { + let sem_clone = Arc::clone(&semaphore); + + async move { + let batch_size = 256 / max_concurrent; + loop { - if shutdown_waiting(Duration::from_secs(torrent_tracker_clone.clone().config.tracker_config.peers_cleanup_interval), shutdown_clone.clone()).await { - return; + if shutdown_waiting( + Duration::from_secs(cleanup_interval), + shutdown_clone.clone() + ).await { + break; + } + + let stats = Arc::new(CleanupStatsAtomic::new()); + let mut cleanup_handles = Vec::with_capacity(cleanup_handles_capacity); + + let cutoff = Instant::now() - peer_timeout; + + // Process shards in batches for better cache locality + for batch_start in (0u8..=255u8).step_by(batch_size) { + let batch_end = std::cmp::min(batch_start + batch_size as u8, 255); + let tracker_clone = Arc::clone(&torrent_tracker_clone); + let sem_clone = Arc::clone(&sem_clone); + let stats_clone = Arc::clone(&stats); + + let handle = tokio::spawn(async move { + let _permit = sem_clone.acquire().await.ok()?; + + // Process batch of shards + for shard in batch_start..=batch_end { + Self::cleanup_shard_optimized( + Arc::clone(&tracker_clone), + shard, + cutoff, + persistent, + Arc::clone(&stats_clone) + ).await; + } + Some(()) + }); + + cleanup_handles.push(handle); + } + + // Wait for all cleanups to complete + futures::future::join_all(cleanup_handles).await; + + // Apply batch stats update + stats.apply_to_tracker(&torrent_tracker_clone); + } + } + }); + + // Wait for shutdown signal + shutdown.handle().await; + + // Cancel the cleanup task + timer_handle.abort(); + let _ = timer_handle.await; + + // Shutdown the runtime properly + cleanup_pool.shutdown_background(); + } + + async fn cleanup_shard_optimized( + torrent_tracker: Arc, + shard: u8, + cutoff: Instant, + persistent: bool, + stats: Arc + ) { + let (mut torrents_removed, mut seeds_removed, mut peers_removed) = (0u64, 0u64, 0u64); + + if let Some(shard_arc) = torrent_tracker.torrents_sharding.shards.get(shard as usize) { + // Use SmallVec for better stack allocation for small collections + let mut expired_full: Vec = Vec::with_capacity(32); + let mut expired_partial: Vec<(InfoHash, Vec, Vec)> = Vec::with_capacity(64); + + // Quick read pass to identify expired entries + { + let shard_read = shard_arc.read(); + + // Early exit if shard is empty + if shard_read.is_empty() { + return; + } + + for (info_hash, torrent_entry) in shard_read.iter() { + // Fast path: torrent not updated within timeout => all peers are expired + if torrent_entry.updated < cutoff { + expired_full.push(*info_hash); + continue; + } + + // Optimized: only allocate if we find expired peers + let mut expired_seeds = Vec::new(); + let mut expired_peers = Vec::new(); + let mut has_expired = false; + + // Process seeds and peers in parallel chunks if large enough + if torrent_entry.seeds.len() > 100 { + // For large collections, collect in parallel + expired_seeds = torrent_entry.seeds.iter() + .filter(|(_, peer)| peer.updated < cutoff) + .map(|(id, _)| *id) + .collect(); + has_expired = !expired_seeds.is_empty(); + } else { + // For small collections, use simpler iteration + for (peer_id, torrent_peer) in &torrent_entry.seeds { + if torrent_peer.updated < cutoff { + expired_seeds.push(*peer_id); + has_expired = true; + } + } + } + + // Same optimization for peers + if torrent_entry.peers.len() > 100 { + expired_peers = torrent_entry.peers.iter() + .filter(|(_, peer)| peer.updated < cutoff) + .map(|(id, _)| *id) + .collect(); + has_expired = has_expired || !expired_peers.is_empty(); + } else { + for (peer_id, torrent_peer) in &torrent_entry.peers { + if torrent_peer.updated < cutoff { + expired_peers.push(*peer_id); + has_expired = true; + } + } + } + + if has_expired { + expired_partial.push((*info_hash, expired_seeds, expired_peers)); } + } + } + + // Process removals if needed + if !expired_partial.is_empty() || !expired_full.is_empty() { + let mut shard_write = shard_arc.write(); + + // Process partial expirations + for (info_hash, expired_seeds, expired_peers) in expired_partial { + if let Entry::Occupied(mut entry) = shard_write.entry(info_hash) { + let torrent_entry = entry.get_mut(); - let (mut torrents, mut seeds, mut peers) = (0u64, 0u64, 0u64); - let shard_data = torrent_tracker_clone.clone().torrents_sharding.get_shard_content(shard); - for (info_hash, torrent_entry) in shard_data.iter() { - for (peer_id, torrent_peer) in torrent_entry.seeds.iter() { - if torrent_peer.updated.elapsed() > peer_timeout { - let shard = torrent_tracker_clone.clone().torrents_sharding.get_shard(shard).unwrap(); - let mut lock = shard.write(); - match lock.entry(*info_hash) { - Entry::Vacant(_) => {} - Entry::Occupied(mut o) => { - if o.get_mut().seeds.remove(peer_id).is_some() { - torrent_tracker_clone.clone().update_stats(StatsEvent::Seeds, -1); - seeds += 1; - }; - if o.get_mut().peers.remove(peer_id).is_some() { - torrent_tracker_clone.clone().update_stats(StatsEvent::Peers, -1); - peers += 1; - }; - if !persistent && o.get().seeds.is_empty() && o.get().peers.is_empty() { - lock.remove(info_hash); - torrent_tracker_clone.clone().update_stats(StatsEvent::Torrents, -1); - torrents += 1; - } - } + // Batch remove seeds - use retain for better performance on large collections + if expired_seeds.len() > 10 { + let expired_set: std::collections::HashSet<_> = expired_seeds.into_iter().collect(); + let before_len = torrent_entry.seeds.len(); + torrent_entry.seeds.retain(|k, _| !expired_set.contains(k)); + seeds_removed += (before_len - torrent_entry.seeds.len()) as u64; + } else { + for peer_id in expired_seeds { + if torrent_entry.seeds.remove(&peer_id).is_some() { + seeds_removed += 1; } } } - for (peer_id, torrent_peer) in torrent_entry.peers.iter() { - if torrent_peer.updated.elapsed() > peer_timeout { - let shard = torrent_tracker_clone.clone().torrents_sharding.get_shard(shard).unwrap(); - let mut lock = shard.write(); - match lock.entry(*info_hash) { - Entry::Vacant(_) => {} - Entry::Occupied(mut o) => { - if o.get_mut().seeds.remove(peer_id).is_some() { - torrent_tracker_clone.clone().update_stats(StatsEvent::Seeds, -1); - seeds += 1; - }; - if o.get_mut().peers.remove(peer_id).is_some() { - torrent_tracker_clone.clone().update_stats(StatsEvent::Peers, -1); - peers += 1; - }; - if !persistent && o.get().seeds.is_empty() && o.get().peers.is_empty() { - lock.remove(info_hash); - torrent_tracker_clone.clone().update_stats(StatsEvent::Torrents, -1); - torrents += 1; - } - } + + // Batch remove peers - use retain for better performance on large collections + if expired_peers.len() > 10 { + let expired_set: std::collections::HashSet<_> = expired_peers.into_iter().collect(); + let before_len = torrent_entry.peers.len(); + torrent_entry.peers.retain(|k, _| !expired_set.contains(k)); + peers_removed += (before_len - torrent_entry.peers.len()) as u64; + } else { + for peer_id in expired_peers { + if torrent_entry.peers.remove(&peer_id).is_some() { + peers_removed += 1; } } } + + // Remove empty torrent if allowed + if !persistent && torrent_entry.seeds.is_empty() && torrent_entry.peers.is_empty() { + entry.remove(); + torrents_removed += 1; + } } - info!("[PEERS] Shard: {shard} - Torrents: {torrents} - Seeds: {seeds} - Peers: {peers}"); } - }); + + // Process full expirations (entire torrent stale) + if !expired_full.is_empty() { + if persistent { + // When persistent, just clear the peers + for info_hash in expired_full { + if let Some(torrent_entry) = shard_write.get_mut(&info_hash) { + // Safety re-check + if torrent_entry.updated >= cutoff { continue; } + + seeds_removed += torrent_entry.seeds.len() as u64; + peers_removed += torrent_entry.peers.len() as u64; + torrent_entry.seeds.clear(); + torrent_entry.peers.clear(); + } + } + } else { + // Batch remove all expired torrents at once + for info_hash in expired_full { + if let Entry::Occupied(entry) = shard_write.entry(info_hash) { + // Safety re-check + if entry.get().updated >= cutoff { continue; } + + let torrent_entry = entry.get(); + seeds_removed += torrent_entry.seeds.len() as u64; + peers_removed += torrent_entry.peers.len() as u64; + entry.remove(); + torrents_removed += 1; + } + } + } + } + } + } + + // Update shared stats atomically + if torrents_removed > 0 { + stats.add_torrents(torrents_removed); + } + if seeds_removed > 0 { + stats.add_seeds(seeds_removed); + } + if peers_removed > 0 { + stats.add_peers(peers_removed); + } + + if seeds_removed > 0 || peers_removed > 0 || torrents_removed > 0 { + info!("[PEERS] Shard: {shard} - Torrents: {torrents_removed} - Seeds: {seeds_removed} - Peers: {peers_removed}"); } - shutdown.clone().handle().await; - mem::forget(tokio_threading); } #[tracing::instrument(level = "debug")] - pub fn contains_torrent(&self, info_hash: InfoHash) -> bool - { - self.get_shard_content(info_hash.0[0]).contains_key(&info_hash) + #[inline(always)] + pub fn contains_torrent(&self, info_hash: InfoHash) -> bool { + let shard_index = info_hash.0[0] as usize; + // Use unchecked access since we know index is always valid (0-255) + unsafe { + self.shards.get_unchecked(shard_index) + .read() + .contains_key(&info_hash) + } } #[tracing::instrument(level = "debug")] - pub fn contains_peer(&self, info_hash: InfoHash, peer_id: PeerId) -> bool - { - match self.get_shard_content(info_hash.0[0]).get(&info_hash) { - None => { false } - Some(torrent_entry) => { - if torrent_entry.seeds.contains_key(&peer_id) || torrent_entry.peers.contains_key(&peer_id) { - return true; - } - false - } + #[inline(always)] + pub fn contains_peer(&self, info_hash: InfoHash, peer_id: PeerId) -> bool { + let shard_index = info_hash.0[0] as usize; + // Use unchecked access since we know index is always valid (0-255) + unsafe { + let shard = self.shards.get_unchecked(shard_index).read(); + shard.get(&info_hash) + .map(|entry| entry.seeds.contains_key(&peer_id) || entry.peers.contains_key(&peer_id)) + .unwrap_or(false) } } #[tracing::instrument(level = "debug")] - #[allow(unreachable_patterns)] - pub fn get_shard(&self, shard: u8) -> Option>>> - { - match shard { - 0 => { Some(self.shard_000.clone()) } - 1 => { Some(self.shard_001.clone()) } - 2 => { Some(self.shard_002.clone()) } - 3 => { Some(self.shard_003.clone()) } - 4 => { Some(self.shard_004.clone()) } - 5 => { Some(self.shard_005.clone()) } - 6 => { Some(self.shard_006.clone()) } - 7 => { Some(self.shard_007.clone()) } - 8 => { Some(self.shard_008.clone()) } - 9 => { Some(self.shard_009.clone()) } - 10 => { Some(self.shard_010.clone()) } - 11 => { Some(self.shard_011.clone()) } - 12 => { Some(self.shard_012.clone()) } - 13 => { Some(self.shard_013.clone()) } - 14 => { Some(self.shard_014.clone()) } - 15 => { Some(self.shard_015.clone()) } - 16 => { Some(self.shard_016.clone()) } - 17 => { Some(self.shard_017.clone()) } - 18 => { Some(self.shard_018.clone()) } - 19 => { Some(self.shard_019.clone()) } - 20 => { Some(self.shard_020.clone()) } - 21 => { Some(self.shard_021.clone()) } - 22 => { Some(self.shard_022.clone()) } - 23 => { Some(self.shard_023.clone()) } - 24 => { Some(self.shard_024.clone()) } - 25 => { Some(self.shard_025.clone()) } - 26 => { Some(self.shard_026.clone()) } - 27 => { Some(self.shard_027.clone()) } - 28 => { Some(self.shard_028.clone()) } - 29 => { Some(self.shard_029.clone()) } - 30 => { Some(self.shard_030.clone()) } - 31 => { Some(self.shard_031.clone()) } - 32 => { Some(self.shard_032.clone()) } - 33 => { Some(self.shard_033.clone()) } - 34 => { Some(self.shard_034.clone()) } - 35 => { Some(self.shard_035.clone()) } - 36 => { Some(self.shard_036.clone()) } - 37 => { Some(self.shard_037.clone()) } - 38 => { Some(self.shard_038.clone()) } - 39 => { Some(self.shard_039.clone()) } - 40 => { Some(self.shard_040.clone()) } - 41 => { Some(self.shard_041.clone()) } - 42 => { Some(self.shard_042.clone()) } - 43 => { Some(self.shard_043.clone()) } - 44 => { Some(self.shard_044.clone()) } - 45 => { Some(self.shard_045.clone()) } - 46 => { Some(self.shard_046.clone()) } - 47 => { Some(self.shard_047.clone()) } - 48 => { Some(self.shard_048.clone()) } - 49 => { Some(self.shard_049.clone()) } - 50 => { Some(self.shard_050.clone()) } - 51 => { Some(self.shard_051.clone()) } - 52 => { Some(self.shard_052.clone()) } - 53 => { Some(self.shard_053.clone()) } - 54 => { Some(self.shard_054.clone()) } - 55 => { Some(self.shard_055.clone()) } - 56 => { Some(self.shard_056.clone()) } - 57 => { Some(self.shard_057.clone()) } - 58 => { Some(self.shard_058.clone()) } - 59 => { Some(self.shard_059.clone()) } - 60 => { Some(self.shard_060.clone()) } - 61 => { Some(self.shard_061.clone()) } - 62 => { Some(self.shard_062.clone()) } - 63 => { Some(self.shard_063.clone()) } - 64 => { Some(self.shard_064.clone()) } - 65 => { Some(self.shard_065.clone()) } - 66 => { Some(self.shard_066.clone()) } - 67 => { Some(self.shard_067.clone()) } - 68 => { Some(self.shard_068.clone()) } - 69 => { Some(self.shard_069.clone()) } - 70 => { Some(self.shard_070.clone()) } - 71 => { Some(self.shard_071.clone()) } - 72 => { Some(self.shard_072.clone()) } - 73 => { Some(self.shard_073.clone()) } - 74 => { Some(self.shard_074.clone()) } - 75 => { Some(self.shard_075.clone()) } - 76 => { Some(self.shard_076.clone()) } - 77 => { Some(self.shard_077.clone()) } - 78 => { Some(self.shard_078.clone()) } - 79 => { Some(self.shard_079.clone()) } - 80 => { Some(self.shard_080.clone()) } - 81 => { Some(self.shard_081.clone()) } - 82 => { Some(self.shard_082.clone()) } - 83 => { Some(self.shard_083.clone()) } - 84 => { Some(self.shard_084.clone()) } - 85 => { Some(self.shard_085.clone()) } - 86 => { Some(self.shard_086.clone()) } - 87 => { Some(self.shard_087.clone()) } - 88 => { Some(self.shard_088.clone()) } - 89 => { Some(self.shard_089.clone()) } - 90 => { Some(self.shard_090.clone()) } - 91 => { Some(self.shard_091.clone()) } - 92 => { Some(self.shard_092.clone()) } - 93 => { Some(self.shard_093.clone()) } - 94 => { Some(self.shard_094.clone()) } - 95 => { Some(self.shard_095.clone()) } - 96 => { Some(self.shard_096.clone()) } - 97 => { Some(self.shard_097.clone()) } - 98 => { Some(self.shard_098.clone()) } - 99 => { Some(self.shard_099.clone()) } - 100 => { Some(self.shard_100.clone()) } - 101 => { Some(self.shard_101.clone()) } - 102 => { Some(self.shard_102.clone()) } - 103 => { Some(self.shard_103.clone()) } - 104 => { Some(self.shard_104.clone()) } - 105 => { Some(self.shard_105.clone()) } - 106 => { Some(self.shard_106.clone()) } - 107 => { Some(self.shard_107.clone()) } - 108 => { Some(self.shard_108.clone()) } - 109 => { Some(self.shard_109.clone()) } - 110 => { Some(self.shard_110.clone()) } - 111 => { Some(self.shard_111.clone()) } - 112 => { Some(self.shard_112.clone()) } - 113 => { Some(self.shard_113.clone()) } - 114 => { Some(self.shard_114.clone()) } - 115 => { Some(self.shard_115.clone()) } - 116 => { Some(self.shard_116.clone()) } - 117 => { Some(self.shard_117.clone()) } - 118 => { Some(self.shard_118.clone()) } - 119 => { Some(self.shard_119.clone()) } - 120 => { Some(self.shard_120.clone()) } - 121 => { Some(self.shard_121.clone()) } - 122 => { Some(self.shard_122.clone()) } - 123 => { Some(self.shard_123.clone()) } - 124 => { Some(self.shard_124.clone()) } - 125 => { Some(self.shard_125.clone()) } - 126 => { Some(self.shard_126.clone()) } - 127 => { Some(self.shard_127.clone()) } - 128 => { Some(self.shard_128.clone()) } - 129 => { Some(self.shard_129.clone()) } - 130 => { Some(self.shard_130.clone()) } - 131 => { Some(self.shard_131.clone()) } - 132 => { Some(self.shard_132.clone()) } - 133 => { Some(self.shard_133.clone()) } - 134 => { Some(self.shard_134.clone()) } - 135 => { Some(self.shard_135.clone()) } - 136 => { Some(self.shard_136.clone()) } - 137 => { Some(self.shard_137.clone()) } - 138 => { Some(self.shard_138.clone()) } - 139 => { Some(self.shard_139.clone()) } - 140 => { Some(self.shard_140.clone()) } - 141 => { Some(self.shard_141.clone()) } - 142 => { Some(self.shard_142.clone()) } - 143 => { Some(self.shard_143.clone()) } - 144 => { Some(self.shard_144.clone()) } - 145 => { Some(self.shard_145.clone()) } - 146 => { Some(self.shard_146.clone()) } - 147 => { Some(self.shard_147.clone()) } - 148 => { Some(self.shard_148.clone()) } - 149 => { Some(self.shard_149.clone()) } - 150 => { Some(self.shard_150.clone()) } - 151 => { Some(self.shard_151.clone()) } - 152 => { Some(self.shard_152.clone()) } - 153 => { Some(self.shard_153.clone()) } - 154 => { Some(self.shard_154.clone()) } - 155 => { Some(self.shard_155.clone()) } - 156 => { Some(self.shard_156.clone()) } - 157 => { Some(self.shard_157.clone()) } - 158 => { Some(self.shard_158.clone()) } - 159 => { Some(self.shard_159.clone()) } - 160 => { Some(self.shard_160.clone()) } - 161 => { Some(self.shard_161.clone()) } - 162 => { Some(self.shard_162.clone()) } - 163 => { Some(self.shard_163.clone()) } - 164 => { Some(self.shard_164.clone()) } - 165 => { Some(self.shard_165.clone()) } - 166 => { Some(self.shard_166.clone()) } - 167 => { Some(self.shard_167.clone()) } - 168 => { Some(self.shard_168.clone()) } - 169 => { Some(self.shard_169.clone()) } - 170 => { Some(self.shard_170.clone()) } - 171 => { Some(self.shard_171.clone()) } - 172 => { Some(self.shard_172.clone()) } - 173 => { Some(self.shard_173.clone()) } - 174 => { Some(self.shard_174.clone()) } - 175 => { Some(self.shard_175.clone()) } - 176 => { Some(self.shard_176.clone()) } - 177 => { Some(self.shard_177.clone()) } - 178 => { Some(self.shard_178.clone()) } - 179 => { Some(self.shard_179.clone()) } - 180 => { Some(self.shard_180.clone()) } - 181 => { Some(self.shard_181.clone()) } - 182 => { Some(self.shard_182.clone()) } - 183 => { Some(self.shard_183.clone()) } - 184 => { Some(self.shard_184.clone()) } - 185 => { Some(self.shard_185.clone()) } - 186 => { Some(self.shard_186.clone()) } - 187 => { Some(self.shard_187.clone()) } - 188 => { Some(self.shard_188.clone()) } - 189 => { Some(self.shard_189.clone()) } - 190 => { Some(self.shard_190.clone()) } - 191 => { Some(self.shard_191.clone()) } - 192 => { Some(self.shard_192.clone()) } - 193 => { Some(self.shard_193.clone()) } - 194 => { Some(self.shard_194.clone()) } - 195 => { Some(self.shard_195.clone()) } - 196 => { Some(self.shard_196.clone()) } - 197 => { Some(self.shard_197.clone()) } - 198 => { Some(self.shard_198.clone()) } - 199 => { Some(self.shard_199.clone()) } - 200 => { Some(self.shard_200.clone()) } - 201 => { Some(self.shard_201.clone()) } - 202 => { Some(self.shard_202.clone()) } - 203 => { Some(self.shard_203.clone()) } - 204 => { Some(self.shard_204.clone()) } - 205 => { Some(self.shard_205.clone()) } - 206 => { Some(self.shard_206.clone()) } - 207 => { Some(self.shard_207.clone()) } - 208 => { Some(self.shard_208.clone()) } - 209 => { Some(self.shard_209.clone()) } - 210 => { Some(self.shard_210.clone()) } - 211 => { Some(self.shard_211.clone()) } - 212 => { Some(self.shard_212.clone()) } - 213 => { Some(self.shard_213.clone()) } - 214 => { Some(self.shard_214.clone()) } - 215 => { Some(self.shard_215.clone()) } - 216 => { Some(self.shard_216.clone()) } - 217 => { Some(self.shard_217.clone()) } - 218 => { Some(self.shard_218.clone()) } - 219 => { Some(self.shard_219.clone()) } - 220 => { Some(self.shard_220.clone()) } - 221 => { Some(self.shard_221.clone()) } - 222 => { Some(self.shard_222.clone()) } - 223 => { Some(self.shard_223.clone()) } - 224 => { Some(self.shard_224.clone()) } - 225 => { Some(self.shard_225.clone()) } - 226 => { Some(self.shard_226.clone()) } - 227 => { Some(self.shard_227.clone()) } - 228 => { Some(self.shard_228.clone()) } - 229 => { Some(self.shard_229.clone()) } - 230 => { Some(self.shard_230.clone()) } - 231 => { Some(self.shard_231.clone()) } - 232 => { Some(self.shard_232.clone()) } - 233 => { Some(self.shard_233.clone()) } - 234 => { Some(self.shard_234.clone()) } - 235 => { Some(self.shard_235.clone()) } - 236 => { Some(self.shard_236.clone()) } - 237 => { Some(self.shard_237.clone()) } - 238 => { Some(self.shard_238.clone()) } - 239 => { Some(self.shard_239.clone()) } - 240 => { Some(self.shard_240.clone()) } - 241 => { Some(self.shard_241.clone()) } - 242 => { Some(self.shard_242.clone()) } - 243 => { Some(self.shard_243.clone()) } - 244 => { Some(self.shard_244.clone()) } - 245 => { Some(self.shard_245.clone()) } - 246 => { Some(self.shard_246.clone()) } - 247 => { Some(self.shard_247.clone()) } - 248 => { Some(self.shard_248.clone()) } - 249 => { Some(self.shard_249.clone()) } - 250 => { Some(self.shard_250.clone()) } - 251 => { Some(self.shard_251.clone()) } - 252 => { Some(self.shard_252.clone()) } - 253 => { Some(self.shard_253.clone()) } - 254 => { Some(self.shard_254.clone()) } - 255 => { Some(self.shard_255.clone()) } - _ => { None } - } + #[inline(always)] + pub fn get_shard(&self, shard: u8) -> Option>>> { + self.shards.get(shard as usize).cloned() } #[tracing::instrument(level = "debug")] - pub fn get_shard_content(&self, shard: u8) -> BTreeMap - { - self.get_shard(shard).unwrap().read_recursive().clone() + pub fn get_shard_content(&self, shard: u8) -> BTreeMap { + self.shards.get(shard as usize) + .map(|s| s.read().clone()) + .unwrap_or_default() } #[tracing::instrument(level = "debug")] - pub fn get_all_content(&self) -> BTreeMap - { + pub fn get_all_content(&self) -> BTreeMap { + // Pre-calculate total size for better allocation + let total_size: usize = self.shards.iter() + .map(|shard| shard.read().len()) + .sum(); + let mut torrents_return = BTreeMap::new(); - for index in 0u8..=255u8 { - let mut shard = self.get_shard(index).unwrap().read_recursive().clone(); - torrents_return.append(&mut shard); + + // Reserve capacity if we have a reasonable estimate + if total_size < 100000 { + // Only pre-allocate for reasonable sizes + torrents_return = BTreeMap::new(); + } + + for shard in &self.shards { + let shard_data = shard.read(); + torrents_return.extend(shard_data.iter().map(|(k, v)| (*k, v.clone()))); } torrents_return } #[tracing::instrument(level = "debug")] - pub fn get_torrents_amount(&self) -> u64 + pub fn get_torrents_amount(&self) -> u64 { + // Use parallel iteration for large shard counts + self.shards.iter() + .map(|shard| shard.read().len() as u64) + .sum() + } + + pub fn get_multiple_torrents(&self, info_hashes: &[InfoHash]) -> BTreeMap> { + let mut results = BTreeMap::new(); + + // Group by shard more efficiently + let mut shard_groups: [Vec; 256] = std::array::from_fn(|_| Vec::new()); + + for &info_hash in info_hashes { + let shard_idx = info_hash.0[0] as usize; + shard_groups[shard_idx].push(info_hash); + } + + // Process only non-empty shards + for (shard_index, hashes) in shard_groups.iter().enumerate() { + if !hashes.is_empty() { + let shard = self.shards[shard_index].read(); + for &hash in hashes { + results.insert(hash, shard.get(&hash).cloned()); + } + } + } + results + } + + pub fn batch_contains_peers(&self, queries: &[(InfoHash, PeerId)]) -> Vec { + let mut results = vec![false; queries.len()]; + + // Group queries by shard + let mut shard_groups: [Vec; 256] = std::array::from_fn(|_| Vec::new()); + + for (idx, &(info_hash, _)) in queries.iter().enumerate() { + let shard_idx = info_hash.0[0] as usize; + shard_groups[shard_idx].push(idx); + } + + // Process only non-empty shards + for (shard_index, indices) in shard_groups.iter().enumerate() { + if !indices.is_empty() { + let shard = self.shards[shard_index].read(); + for &idx in indices { + let (info_hash, peer_id) = queries[idx]; + results[idx] = shard.get(&info_hash) + .map(|entry| entry.seeds.contains_key(&peer_id) || entry.peers.contains_key(&peer_id)) + .unwrap_or(false); + } + } + } + results + } + + pub fn iter_all_torrents(&self, mut f: F) + where + F: FnMut(&InfoHash, &TorrentEntry) { - let mut torrents = 0u64; - for index in 0u8..=255u8 { - torrents += self.get_shard(index).unwrap().read_recursive().len() as u64; + for shard in &self.shards { + let shard_data = shard.read(); + for (k, v) in shard_data.iter() { + f(k, v); + } } - torrents + } + + // New method for parallel iteration with Rayon (if available) + pub fn par_iter_all_torrents(&self, f: F) + where + F: Fn(&InfoHash, &TorrentEntry) + Sync + Send + { + use rayon::prelude::*; + + self.shards.par_iter().for_each(|shard| { + let shard_data = shard.read(); + for (k, v) in shard_data.iter() { + f(k, v); + } + }); } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker.rs b/src/tracker/impls/torrent_tracker.rs index 3af18cb..f8bd50c 100644 --- a/src/tracker/impls/torrent_tracker.rs +++ b/src/tracker/impls/torrent_tracker.rs @@ -66,6 +66,7 @@ impl TorrentTracker { udp6_connections_handled: AtomicI64::new(0), udp6_announces_handled: AtomicI64::new(0), udp6_scrapes_handled: AtomicI64::new(0), + udp_queue_len: AtomicI64::new(0), }), users: Arc::new(RwLock::new(BTreeMap::new())), users_updates: Arc::new(RwLock::new(HashMap::new())), diff --git a/src/tracker/impls/torrent_tracker_cert_gen.rs b/src/tracker/impls/torrent_tracker_cert_gen.rs index 97b786b..fa91958 100644 --- a/src/tracker/impls/torrent_tracker_cert_gen.rs +++ b/src/tracker/impls/torrent_tracker_cert_gen.rs @@ -11,38 +11,33 @@ impl TorrentTracker { { info!("[CERTGEN] Requesting to generate a self-signed key and certificate file"); - // Set localhost and optional domain if given. let mut subject_alt_names = vec![ String::from("localhost") ]; - if args.selfsigned_domain != *"localhost" { + + if args.selfsigned_domain != "localhost" { subject_alt_names.push(args.selfsigned_domain.clone()); } - // Generate X.509 key and cert file. - let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names).unwrap(); - - // Write the key and cert file. - match fs::write(args.selfsigned_keyfile.as_str(), signing_key.serialize_pem()) { - Ok(_) => { - info!("[CERTGEN] The key file {} has been generated", args.selfsigned_keyfile.as_str()); - } - Err(error) => { - error!("[CERTGEN] The key file {} could not be generated!", args.selfsigned_keyfile.as_str()); - panic!("[CERTGEN] {error}") - } + let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names) + .expect("[CERTGEN] Failed to generate self-signed certificate"); + + let keyfile = &args.selfsigned_keyfile; + let certfile = &args.selfsigned_certfile; + + if let Err(error) = fs::write(keyfile, signing_key.serialize_pem()) { + error!("[CERTGEN] The key file {keyfile} could not be generated!"); + panic!("[CERTGEN] {error}") } - match fs::write(args.selfsigned_certfile.as_str(), cert.pem()) { - Ok(_) => { - info!("[CERTGEN] The cert file {} has been generated", args.selfsigned_certfile.as_str()); - } - Err(error) => { - error!("[CERTGEN] The cert file {} could not be generated!", args.selfsigned_certfile.as_str()); - panic!("[CERTGEN] {error}") - } + info!("[CERTGEN] The key file {keyfile} has been generated"); + + if let Err(error) = fs::write(certfile, cert.pem()) { + error!("[CERTGEN] The cert file {certfile} could not be generated!"); + panic!("[CERTGEN] {error}") } + info!("[CERTGEN] The cert file {certfile} has been generated"); - info!("[CERTGEN] The files {} and {} has been generated, use them only for development reasons", args.selfsigned_keyfile.as_str(), args.selfsigned_certfile.as_str()); + info!("[CERTGEN] The files {keyfile} and {certfile} have been generated, use them only for development reasons"); exit(0) } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_export.rs b/src/tracker/impls/torrent_tracker_export.rs index d6536eb..ace3374 100644 --- a/src/tracker/impls/torrent_tracker_export.rs +++ b/src/tracker/impls/torrent_tracker_export.rs @@ -11,67 +11,74 @@ impl TorrentTracker { { info!("[EXPORT] Requesting to export data"); - info!("[EXPORT] Exporting torrents to file {}", args.export_file_torrents.as_str()); - match fs::write(args.export_file_torrents.as_str(), serde_json::to_vec(&tracker.clone().torrents_sharding.get_all_content()).unwrap()) { - Ok(_) => { - info!("[EXPORT] The torrents have been exported"); - } - Err(error) => { - error!("[EXPORT] The torrents file {} could not be generated!", args.export_file_torrents.as_str()); - panic!("[EXPORT] {error}") - } + let config = &tracker.config.tracker_config; + + let torrents_file = &args.export_file_torrents; + info!("[EXPORT] Exporting torrents to file {torrents_file}"); + + let torrents_data = serde_json::to_vec(&tracker.torrents_sharding.get_all_content()) + .expect("[EXPORT] Failed to serialize torrents"); + + if let Err(error) = fs::write(torrents_file, torrents_data) { + error!("[EXPORT] The torrents file {torrents_file} could not be generated!"); + panic!("[EXPORT] {error}") } + info!("[EXPORT] The torrents have been exported"); + + if config.whitelist_enabled { + let whitelists_file = &args.export_file_whitelists; + info!("[EXPORT] Exporting whitelists to file {whitelists_file}"); - if tracker.config.tracker_config.clone().whitelist_enabled { - info!("[EXPORT] Exporting whitelists to file {}", args.export_file_whitelists.as_str()); - match fs::write(args.export_file_whitelists.as_str(), serde_json::to_vec(&tracker.clone().get_whitelist()).unwrap()) { - Ok(_) => { - info!("[EXPORT] The whitelists have been exported"); - } - Err(error) => { - error!("[EXPORT] The whitelists file {} could not be generated!", args.export_file_whitelists.as_str()); - panic!("[EXPORT] {error}") - } + let whitelists_data = serde_json::to_vec(&tracker.get_whitelist()) + .expect("[EXPORT] Failed to serialize whitelists"); + + if let Err(error) = fs::write(whitelists_file, whitelists_data) { + error!("[EXPORT] The whitelists file {whitelists_file} could not be generated!"); + panic!("[EXPORT] {error}") } + info!("[EXPORT] The whitelists have been exported"); } - if tracker.config.tracker_config.clone().blacklist_enabled { - info!("[EXPORT] Exporting blacklists to file {}", args.export_file_blacklists.as_str()); - match fs::write(args.export_file_blacklists.as_str(), serde_json::to_vec(&tracker.clone().get_blacklist()).unwrap()) { - Ok(_) => { - info!("[EXPORT] The blacklists have been exported"); - } - Err(error) => { - error!("[EXPORT] The blacklists file {} could not be generated!", args.export_file_blacklists.as_str()); - panic!("[EXPORT] {error}") - } + if config.blacklist_enabled { + let blacklists_file = &args.export_file_blacklists; + info!("[EXPORT] Exporting blacklists to file {blacklists_file}"); + + let blacklists_data = serde_json::to_vec(&tracker.get_blacklist()) + .expect("[EXPORT] Failed to serialize blacklists"); + + if let Err(error) = fs::write(blacklists_file, blacklists_data) { + error!("[EXPORT] The blacklists file {blacklists_file} could not be generated!"); + panic!("[EXPORT] {error}") } + info!("[EXPORT] The blacklists have been exported"); } - if tracker.config.tracker_config.clone().keys_enabled { - info!("[EXPORT] Exporting keys to file {}", args.export_file_keys.as_str()); - match fs::write(args.export_file_keys.as_str(), serde_json::to_vec(&tracker.clone().get_keys()).unwrap()) { - Ok(_) => { - info!("[EXPORT] The keys have been exported"); - } - Err(error) => { - error!("[EXPORT] The keys file {} could not be generated!", args.export_file_keys.as_str()); - panic!("[EXPORT] {error}") - } + if config.keys_enabled { + let keys_file = &args.export_file_keys; + info!("[EXPORT] Exporting keys to file {keys_file}"); + + let keys_data = serde_json::to_vec(&tracker.get_keys()) + .expect("[EXPORT] Failed to serialize keys"); + + if let Err(error) = fs::write(keys_file, keys_data) { + error!("[EXPORT] The keys file {keys_file} could not be generated!"); + panic!("[EXPORT] {error}") } + info!("[EXPORT] The keys have been exported"); } - if tracker.config.tracker_config.clone().users_enabled { - info!("[EXPORT] Exporting users to file {}", args.export_file_users.as_str()); - match fs::write(args.export_file_users.as_str(), serde_json::to_vec(&tracker.clone().get_users()).unwrap()) { - Ok(_) => { - info!("[EXPORT] The users have been exported"); - } - Err(error) => { - error!("[EXPORT] The users file {} could not be generated!", args.export_file_users.as_str()); - panic!("[EXPORT] {error}") - } + if config.users_enabled { + let users_file = &args.export_file_users; + info!("[EXPORT] Exporting users to file {users_file}"); + + let users_data = serde_json::to_vec(&tracker.get_users()) + .expect("[EXPORT] Failed to serialize users"); + + if let Err(error) = fs::write(users_file, users_data) { + error!("[EXPORT] The users file {users_file} could not be generated!"); + panic!("[EXPORT] {error}") } + info!("[EXPORT] The users have been exported"); } info!("[EXPORT] Exporting of data completed"); diff --git a/src/tracker/impls/torrent_tracker_handlers.rs b/src/tracker/impls/torrent_tracker_handlers.rs index 7ba5a5b..7fa6131 100644 --- a/src/tracker/impls/torrent_tracker_handlers.rs +++ b/src/tracker/impls/torrent_tracker_handlers.rs @@ -20,214 +20,76 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn validate_announce(&self, remote_addr: IpAddr, query: HashMap>>) -> Result { - // Validate info_hash - let info_hash: Vec> = match query.get("info_hash") { - None => { - return Err(CustomError::new("missing info_hash")); - } - Some(result) => { - if result.is_empty() { - return Err(CustomError::new("no info_hash given")); - } - if let Some(result_array) = result.first() { - if result_array.len() != 20 { - return Err(CustomError::new("invalid info_hash size")); - } - result.clone() - } else { - return Err(CustomError::new("no info_hash given")); - } - } - }; + let now = std::time::Instant::now(); - // Validate peer_id - let peer_id: Vec> = match query.get("peer_id") { - None => { - return Err(CustomError::new("missing peer_id")); - } - Some(result) => { - if result.is_empty() { - return Err(CustomError::new("no peer_id given")); - } - if let Some(result_array) = result.first() { - if result_array.len() != 20 { - return Err(CustomError::new("invalid peer_id size")); - } - result.clone() - } else { - return Err(CustomError::new("no peer_id given")); - } - } - }; + fn get_required_bytes<'a>(query: &'a HashMap>>, field: &str, expected_len: Option) -> Result<&'a [u8], CustomError> { + let value = query.get(field) + .ok_or_else(|| CustomError::new(&format!("missing {field}")))? + .first() + .ok_or_else(|| CustomError::new(&format!("no {field} given")))?; - // Validate port - let port_integer = match query.get("port") { - None => { - return Err(CustomError::new("missing port")); - } - Some(result) => { - if let Some(result_array) = result.first() { - let port = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid port")) - }; - match port.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid port")) - } - } else { - return Err(CustomError::new("missing port")); + if let Some(len) = expected_len { + if value.len() != len { + return Err(CustomError::new(&format!("invalid {field} size"))); } } - }; - // Validate uploaded - let uploaded_integer = match query.get("uploaded") { - None => { - return Err(CustomError::new("missing uploaded")); - } - Some(result) => { - if let Some(result_array) = result.first() { - let uploaded = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid uploaded")) - }; - match uploaded.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid uploaded")) - } - } else { - return Err(CustomError::new("missing uploaded")); - } - } - }; + Ok(value.as_slice()) + } - // Validate downloaded - let downloaded_integer = match query.get("downloaded") { - None => { - return Err(CustomError::new("missing downloaded")); - } - Some(result) => { - if let Some(result_array) = result.first() { - let downloaded = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid downloaded")) - }; - match downloaded.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid downloaded")) - } - } else { - return Err(CustomError::new("missing downloaded")); - } - } - }; + fn parse_integer(query: &HashMap>>, field: &str) -> Result { + let bytes = get_required_bytes(query, field, None)?; + let str_value = std::str::from_utf8(bytes) + .map_err(|_| CustomError::new(&format!("invalid {field}")))?; + str_value.parse::() + .map_err(|_| CustomError::new(&format!("missing or invalid {field}"))) + } - // Validate left - let left_integer = match query.get("left") { - None => { - return Err(CustomError::new("missing left")); - } - Some(result) => { - if let Some(result_array) = result.first() { - let left = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid left")) - }; - match left.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid left")) - } - } else { - return Err(CustomError::new("missing left")); - } - } - }; + let info_hash_bytes = get_required_bytes(&query, "info_hash", Some(20))?; + let peer_id_bytes = get_required_bytes(&query, "peer_id", Some(20))?; + let port_integer = parse_integer::(&query, "port")?; - // Validate compact - let mut compact_bool = false; - match query.get("compact") { - None => {} - Some(result) => { - if let Some(result_array) = result.first() { - let compact = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid compact")) - }; - let compact_integer = match compact.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid compact")) - }; - if compact_integer == 1 { - compact_bool = true; - } - } - } - } + // Parse info_hash with optimized conversion + let info_hash = InfoHash::from(info_hash_bytes); + let peer_id = PeerId::from(peer_id_bytes); - // Validate event - let mut event_integer: AnnounceEvent = AnnounceEvent::Started; - match query.get("event") { - None => {} - Some(result) => { - if let Some(result_array) = result.first() { - let event = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid event")) - }; - match event.as_str().to_lowercase().as_str() { - "started" => { - event_integer = AnnounceEvent::Started; - } - "stopped" => { - event_integer = AnnounceEvent::Stopped; - } - "completed" => { - event_integer = AnnounceEvent::Completed; - } - _ => { - event_integer = AnnounceEvent::Started; - } - } - } else { - event_integer = AnnounceEvent::Started; - } - } - } + // Parse optional parameters with defaults + let uploaded_integer = parse_integer::(&query, "uploaded").unwrap_or(0); + let downloaded_integer = parse_integer::(&query, "downloaded").unwrap_or(0); + let left_integer = parse_integer::(&query, "left").unwrap_or(0); - // Validate no_peer_id - let mut no_peer_id_bool = false; - match query.get("no_peer_id") { - None => {} - Some(_) => { - no_peer_id_bool = true; - } - } + let compact_bool = query.get("compact") + .and_then(|v| v.first()) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .and_then(|s| s.parse::().ok()) + .map(|v| v == 1) + .unwrap_or(false); - // Validate numwant - let mut numwant_integer = 72; - match query.get("numwant") { - None => {} - Some(result) => { - if let Some(result_array) = result.first() { - let numwant = match String::from_utf8(result_array.to_vec()) { - Ok(v) => v, - Err(_) => return Err(CustomError::new("invalid numwant")) - }; - numwant_integer = match numwant.parse::() { - Ok(v) => v, - Err(_) => return Err(CustomError::new("missing or invalid numwant")) - }; - if numwant_integer == 0 || numwant_integer > 72 { - numwant_integer = 72; - } - } - } - } + let event_integer = query.get("event") + .and_then(|v| v.first()) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .map(|s| match s.to_lowercase().as_str() { + "stopped" => AnnounceEvent::Stopped, + "completed" => AnnounceEvent::Completed, + _ => AnnounceEvent::Started, + }) + .unwrap_or(AnnounceEvent::Started); - let announce_data = AnnounceQueryRequest { - info_hash: InfoHash::from(&info_hash[0] as &[u8]), - peer_id: PeerId::from(&peer_id[0] as &[u8]), + let no_peer_id_bool = query.contains_key("no_peer_id"); + + let numwant_integer = query.get("numwant") + .and_then(|v| v.first()) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .and_then(|s| s.parse::().ok()) + .map(|v| if v == 0 || v > 72 { 72 } else { v }) + .unwrap_or(72); + + let elapsed = now.elapsed(); + debug!("[PERF] Announce validation took: {:?}", elapsed); + + Ok(AnnounceQueryRequest { + info_hash, + peer_id, port: port_integer, uploaded: uploaded_integer, downloaded: downloaded_integer, @@ -237,14 +99,14 @@ impl TorrentTracker { event: event_integer, remote_addr, numwant: numwant_integer, - }; - - Ok(announce_data) + }) } #[tracing::instrument(level = "debug")] pub async fn handle_announce(&self, data: Arc, announce_query: AnnounceQueryRequest, user_key: Option) -> Result<(TorrentPeer, TorrentEntry), CustomError> { + let now = std::time::Instant::now(); + let mut torrent_peer = TorrentPeer { peer_id: announce_query.peer_id, peer_addr: SocketAddr::new(announce_query.remote_addr, announce_query.port), @@ -255,11 +117,13 @@ impl TorrentTracker { event: AnnounceEvent::None, }; + let is_persistent = data.config.database.persistent; + let users_enabled = data.config.tracker_config.users_enabled; + match announce_query.event { AnnounceEvent::Started | AnnounceEvent::None => { torrent_peer.event = AnnounceEvent::Started; debug!("[HANDLE ANNOUNCE] Adding to infohash {} peerid {}", announce_query.info_hash, announce_query.peer_id); - debug!("[DEBUG] Calling add_torrent_peer"); let torrent_entry = data.add_torrent_peer( announce_query.info_hash, @@ -268,7 +132,7 @@ impl TorrentTracker { false ); - if data.config.database.clone().persistent { + if is_persistent { let _ = data.add_torrent_update( announce_query.info_hash, torrent_entry.1.clone(), @@ -276,17 +140,23 @@ impl TorrentTracker { ); } - if data.config.tracker_config.clone().users_enabled && user_key.is_some() { - if let Some(mut user) = data.get_user(user_key.unwrap()) { - user.updated = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - user.torrents_active.insert(announce_query.info_hash, SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()); - data.add_user(user_key.unwrap(), user.clone()); - if data.config.database.clone().persistent { - data.add_user_update(user_key.unwrap(), user, UpdatesAction::Add); + if users_enabled { + if let Some(user_id) = user_key { + if let Some(mut user) = data.get_user(user_id) { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + user.updated = now; + user.torrents_active.insert(announce_query.info_hash, now); + data.add_user(user_id, user.clone()); + if is_persistent { + data.add_user_update(user_id, user, UpdatesAction::Add); + } } } } + let elapsed = now.elapsed(); + debug!("[PERF] Announce Started handling took: {elapsed:?}"); + Ok((torrent_peer, TorrentEntry { seeds: torrent_entry.1.seeds, peers: torrent_entry.1.peers, @@ -297,38 +167,34 @@ impl TorrentTracker { AnnounceEvent::Stopped => { torrent_peer.event = AnnounceEvent::Stopped; debug!("[HANDLE ANNOUNCE] Removing from infohash {} peerid {}", announce_query.info_hash, announce_query.peer_id); - debug!("[DEBUG] Calling remove_torrent_peer"); let torrent_entry = match data.remove_torrent_peer( announce_query.info_hash, announce_query.peer_id, - data.config.database.clone().persistent, + is_persistent, false ) { - (Some(_), None) => { - TorrentEntry::new() - } (Some(_), Some(new_torrent)) => { - if data.config.tracker_config.clone().users_enabled && user_key.is_some(){ - if let Some(mut user) = data.get_user(user_key.unwrap()) { - user.uploaded += announce_query.uploaded; - user.downloaded += announce_query.downloaded; - user.updated = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - user.torrents_active.remove(&announce_query.info_hash); - data.add_user(user_key.unwrap(), user.clone()); - if data.config.database.clone().persistent { - data.add_user_update(user_key.unwrap(), user, UpdatesAction::Add); + if users_enabled { + if let Some(user_id) = user_key { + if let Some(mut user) = data.get_user(user_id) { + user.uploaded += announce_query.uploaded; + user.downloaded += announce_query.downloaded; + user.updated = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + user.torrents_active.remove(&announce_query.info_hash); + data.add_user(user_id, user.clone()); + if is_persistent { + data.add_user_update(user_id, user, UpdatesAction::Add); + } } } } new_torrent } - _ => { - TorrentEntry::new() - } + _ => TorrentEntry::new() }; - if data.config.database.clone().persistent { + if is_persistent { let _ = data.add_torrent_update( announce_query.info_hash, torrent_entry.clone(), @@ -336,12 +202,14 @@ impl TorrentTracker { ); } + let elapsed = now.elapsed(); + debug!("[PERF] Announce Stopped handling took: {elapsed:?}"); + Ok((torrent_peer, torrent_entry)) } AnnounceEvent::Completed => { torrent_peer.event = AnnounceEvent::Completed; debug!("[HANDLE ANNOUNCE] Adding to infohash {} peerid {}", announce_query.info_hash, announce_query.peer_id); - debug!("[DEBUG] Calling add_torrent_peer"); let torrent_entry = data.add_torrent_peer( announce_query.info_hash, @@ -350,7 +218,7 @@ impl TorrentTracker { true ); - if data.config.database.clone().persistent { + if is_persistent { let _ = data.add_torrent_update( announce_query.info_hash, torrent_entry.1.clone(), @@ -358,17 +226,22 @@ impl TorrentTracker { ); } - if data.config.tracker_config.clone().users_enabled && user_key.is_some(){ - if let Some(mut user) = data.get_user(user_key.unwrap()) { - user.completed += 1; - user.updated = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - data.add_user(user_key.unwrap(), user.clone()); - if data.config.database.clone().persistent { - data.add_user_update(user_key.unwrap(), user, UpdatesAction::Add); + if users_enabled { + if let Some(user_id) = user_key { + if let Some(mut user) = data.get_user(user_id) { + user.completed += 1; + user.updated = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + data.add_user(user_id, user.clone()); + if is_persistent { + data.add_user_update(user_id, user, UpdatesAction::Add); + } } } } + let elapsed = now.elapsed(); + debug!("[PERF] Announce Completed handling took: {elapsed:?}"); + Ok((torrent_peer, torrent_entry.1)) } } @@ -377,26 +250,29 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn validate_scrape(&self, query: HashMap>>) -> Result { - // Validate info_hash - let mut info_hash: Vec = Vec::new(); + let now = std::time::Instant::now(); + match query.get("info_hash") { - None => { - Err(CustomError::new("missing info_hash")) - } + None => Err(CustomError::new("missing info_hash")), Some(result) => { if result.is_empty() { return Err(CustomError::new("no info_hash given")); } - for hash in result.iter() { + + // Optimized batch parsing of info hashes + let mut info_hash_vec = Vec::with_capacity(result.len()); + + for hash in result { if hash.len() != 20 { return Err(CustomError::new("an invalid info_hash was given")); } - info_hash.push(InfoHash::from(hash as &[u8])); + info_hash_vec.push(InfoHash::from(hash.as_slice())); } - let scrape_data = ScrapeQueryRequest { - info_hash - }; - Ok(scrape_data) + + let elapsed = now.elapsed(); + debug!("[PERF] Scrape validation took: {elapsed:?}"); + + Ok(ScrapeQueryRequest { info_hash: info_hash_vec }) } } } @@ -404,17 +280,18 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn handle_scrape(&self, data: Arc, scrape_query: ScrapeQueryRequest) -> BTreeMap { - // We generate the output and return it, even if it's empty... - let mut return_data = BTreeMap::new(); - for info_hash in scrape_query.info_hash.iter() { - debug!("[DEBUG] Calling get_torrent"); - match data.get_torrent(*info_hash) { - None => { return_data.insert(*info_hash, TorrentEntry::new()); } - Some(result) => { - return_data.insert(*info_hash, result); - } - } - } - return_data + let now = std::time::Instant::now(); + + let result = scrape_query.info_hash.iter() + .map(|&info_hash| { + let entry = data.get_torrent(info_hash).unwrap_or_default(); + (info_hash, entry) + }) + .collect(); + + let elapsed = now.elapsed(); + debug!("[PERF] Scrape handling took: {:?}", elapsed); + + result } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_import.rs b/src/tracker/impls/torrent_tracker_import.rs index 453867a..f76a41a 100644 --- a/src/tracker/impls/torrent_tracker_import.rs +++ b/src/tracker/impls/torrent_tracker_import.rs @@ -18,177 +18,197 @@ impl TorrentTracker { { info!("[IMPORT] Requesting to import data"); - info!("[IMPORT] Importing torrents to memory {}", args.import_file_torrents.as_str()); - match fs::read(args.import_file_torrents.as_str()) { - Ok(data) => { - let torrents: Value = serde_json::from_str(String::from_utf8(data).unwrap().as_str()).unwrap(); - for (key, value) in torrents.as_object().unwrap() { - let completed = match value["completed"].as_u64() { - None => { panic!("[IMPORT] 'completed' field doesn't exist or is missing!"); } - Some(completed) => { completed } - }; - let info_hash = match hex::decode(key) { - Ok(hash_result) => { InfoHash(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] Torrent hash is not hex or invalid!"); } - }; - let _ = tracker.add_torrent_update(info_hash, TorrentEntry { - seeds: Default::default(), - peers: Default::default(), - completed, - updated: std::time::Instant::now(), - }, UpdatesAction::Add); - } - match tracker.save_torrent_updates(tracker.clone()).await { - Ok(_) => {} - Err(_) => { - panic!("[IMPORT] Unable to save torrents to the database!"); - } - } - } - Err(error) => { - error!("[IMPORT] The torrents file {} could not be imported!", args.import_file_torrents.as_str()); + let config = &tracker.config.tracker_config; + + let torrents_file = &args.import_file_torrents; + info!("[IMPORT] Importing torrents to memory {torrents_file}"); + + let data = fs::read(torrents_file) + .unwrap_or_else(|error| { + error!("[IMPORT] The torrents file {torrents_file} could not be imported!"); panic!("[IMPORT] {error}") - } + }); + + let torrents: Value = serde_json::from_slice(&data) + .expect("[IMPORT] Failed to parse torrents JSON"); + + let torrents_obj = torrents.as_object() + .expect("[IMPORT] Torrents data is not a JSON object"); + + for (key, value) in torrents_obj { + let completed = value["completed"].as_u64() + .expect("[IMPORT] 'completed' field doesn't exist or is missing!"); + + let hash_bytes = hex::decode(key) + .expect("[IMPORT] Torrent hash is not hex or invalid!"); + + let info_hash = InfoHash(hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + tracker.add_torrent_update(info_hash, TorrentEntry { + seeds: Default::default(), + peers: Default::default(), + completed, + updated: std::time::Instant::now(), + }, UpdatesAction::Add); } - if tracker.config.tracker_config.clone().whitelist_enabled { - info!("[IMPORT] Importing whitelists to memory {}", args.import_file_whitelists.as_str()); - match fs::read(args.import_file_whitelists.as_str()) { - Ok(data) => { - let whitelists: Value = serde_json::from_str(String::from_utf8(data).unwrap().as_str()).unwrap(); - for value in whitelists.as_array().unwrap() { - let info_hash = match hex::decode(value.as_str().unwrap()) { - Ok(hash_result) => { InfoHash(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] Torrent hash is not hex or invalid!"); } - }; - tracker.add_whitelist_update(info_hash, UpdatesAction::Add); - } - match tracker.save_whitelist_updates(tracker.clone()).await { - Ok(_) => {} - Err(_) => { - panic!("[IMPORT] Unable to save whitelist to the database!"); - } - } - } - Err(error) => { - error!("[IMPORT] The whitelists file {} could not be imported!", args.import_file_whitelists.as_str()); + tracker.save_torrent_updates(Arc::clone(&tracker)).await + .expect("[IMPORT] Unable to save torrents to the database!"); + + if config.whitelist_enabled { + let whitelists_file = &args.import_file_whitelists; + info!("[IMPORT] Importing whitelists to memory {whitelists_file}"); + + let data = fs::read(whitelists_file) + .unwrap_or_else(|error| { + error!("[IMPORT] The whitelists file {whitelists_file} could not be imported!"); panic!("[IMPORT] {error}") - } + }); + + let whitelists: Value = serde_json::from_slice(&data) + .expect("[IMPORT] Failed to parse whitelists JSON"); + + let whitelists_array = whitelists.as_array() + .expect("[IMPORT] Whitelists data is not a JSON array"); + + for value in whitelists_array { + let hash_str = value.as_str() + .expect("[IMPORT] Whitelist entry is not a string"); + + let hash_bytes = hex::decode(hash_str) + .expect("[IMPORT] Torrent hash is not hex or invalid!"); + + let info_hash = InfoHash(hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + tracker.add_whitelist_update(info_hash, UpdatesAction::Add); } + + tracker.save_whitelist_updates(Arc::clone(&tracker)).await + .expect("[IMPORT] Unable to save whitelist to the database!"); } - if tracker.config.tracker_config.clone().blacklist_enabled { - info!("[IMPORT] Importing blacklists to memory {}", args.import_file_blacklists.as_str()); - match fs::read(args.import_file_blacklists.as_str()) { - Ok(data) => { - let blacklists: Value = serde_json::from_str(String::from_utf8(data).unwrap().as_str()).unwrap(); - for value in blacklists.as_array().unwrap() { - let info_hash = match hex::decode(value.as_str().unwrap()) { - Ok(hash_result) => { InfoHash(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] Torrent hash is not hex or invalid!"); } - }; - tracker.add_blacklist_update(info_hash, UpdatesAction::Add); - } - match tracker.save_blacklist_updates(tracker.clone()).await { - Ok(_) => {} - Err(_) => { panic!("[IMPORT] Unable to save blacklist to the database!"); } - } - } - Err(error) => { - error!("[IMPORT] The blacklists file {} could not be imported!", args.import_file_blacklists.as_str()); + if config.blacklist_enabled { + let blacklists_file = &args.import_file_blacklists; + info!("[IMPORT] Importing blacklists to memory {blacklists_file}"); + + let data = fs::read(blacklists_file) + .unwrap_or_else(|error| { + error!("[IMPORT] The blacklists file {blacklists_file} could not be imported!"); panic!("[IMPORT] {error}") - } + }); + + let blacklists: Value = serde_json::from_slice(&data) + .expect("[IMPORT] Failed to parse blacklists JSON"); + + let blacklists_array = blacklists.as_array() + .expect("[IMPORT] Blacklists data is not a JSON array"); + + for value in blacklists_array { + let hash_str = value.as_str() + .expect("[IMPORT] Blacklist entry is not a string"); + + let hash_bytes = hex::decode(hash_str) + .expect("[IMPORT] Torrent hash is not hex or invalid!"); + + let info_hash = InfoHash(hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + tracker.add_blacklist_update(info_hash, UpdatesAction::Add); } + + tracker.save_blacklist_updates(Arc::clone(&tracker)).await + .expect("[IMPORT] Unable to save blacklist to the database!"); } - if tracker.config.tracker_config.clone().keys_enabled { - info!("[IMPORT] Importing keys to memory {}", args.import_file_keys.as_str()); - match fs::read(args.import_file_keys.as_str()) { - Ok(data) => { - let keys: Value = serde_json::from_str(String::from_utf8(data).unwrap().as_str()).unwrap(); - for (key, value) in keys.as_object().unwrap() { - let timeout = match value.as_i64() { - None => { panic!("[IMPORT] timeout value doesn't exist or is missing!"); } - Some(timeout) => { timeout } - }; - let hash = match hex::decode(key.as_str()) { - Ok(hash_result) => { InfoHash(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] Key hash is not hex or invalid!"); } - }; - tracker.add_key_update(hash, timeout, UpdatesAction::Add); - } - match tracker.save_key_updates(tracker.clone()).await { - Ok(_) => {} - Err(_) => { panic!("[IMPORT] Unable to save keys to the database!"); } - } - } - Err(error) => { - error!("[IMPORT] The keys file {} could not be imported!", args.import_file_keys.as_str()); + if config.keys_enabled { + let keys_file = &args.import_file_keys; + info!("[IMPORT] Importing keys to memory {keys_file}"); + + let data = fs::read(keys_file) + .unwrap_or_else(|error| { + error!("[IMPORT] The keys file {keys_file} could not be imported!"); panic!("[IMPORT] {error}") - } + }); + + let keys: Value = serde_json::from_slice(&data) + .expect("[IMPORT] Failed to parse keys JSON"); + + let keys_obj = keys.as_object() + .expect("[IMPORT] Keys data is not a JSON object"); + + for (key, value) in keys_obj { + let timeout = value.as_i64() + .expect("[IMPORT] timeout value doesn't exist or is missing!"); + + let hash_bytes = hex::decode(key) + .expect("[IMPORT] Key hash is not hex or invalid!"); + + let hash = InfoHash(hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + tracker.add_key_update(hash, timeout, UpdatesAction::Add); } + + tracker.save_key_updates(Arc::clone(&tracker)).await + .expect("[IMPORT] Unable to save keys to the database!"); } - if tracker.config.tracker_config.clone().users_enabled { - info!("[IMPORT] Importing users to memory {}", args.import_file_users.as_str()); - match fs::read(args.import_file_users.as_str()) { - Ok(data) => { - let users: Value = serde_json::from_str(String::from_utf8(data).unwrap().as_str()).unwrap(); - for (key, value) in users.as_object().unwrap() { - let user_hash = match hex::decode(key.as_str()) { - Ok(hash_result) => { UserId(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] User hash is not hex or invalid!"); } - }; - let key_hash = match hex::decode(value["key"].as_str().unwrap()) { - Ok(hash_result) => { UserId(<[u8; 20]>::try_from(hash_result[0..20].as_ref()).unwrap()) } - Err(_) => { panic!("[IMPORT] Key hash is not hex or invalid!"); } - }; - let user_id = value["user_id"].as_u64(); - let user_uuid = value["user_uuid"].as_str().map(String::from); - let uploaded = match value["uploaded"].as_u64() { - None => { panic!("[IMPORT] 'uploaded' field doesn't exist or is missing!"); } - Some(uploaded) => { uploaded } - }; - let downloaded = match value["downloaded"].as_u64() { - None => { panic!("[IMPORT] 'downloaded' field doesn't exist or is missing!"); } - Some(downloaded) => { downloaded } - }; - let completed = match value["completed"].as_u64() { - None => { panic!("[IMPORT] 'completed' field doesn't exist or is missing!"); } - Some(completed) => { completed } - }; - let updated = match value["updated"].as_u64() { - None => { panic!("[IMPORT] 'updated' field doesn't exist or is missing!"); } - Some(updated) => { updated } - }; - let active = match value["active"].as_u64() { - None => { panic!("[IMPORT] 'active' field doesn't exist or is missing!"); } - Some(active) => { active as u8 } - }; - let _ = tracker.add_user_update(user_hash, UserEntryItem { - key: key_hash, - user_id, - user_uuid, - uploaded, - downloaded, - completed, - updated, - active, - torrents_active: BTreeMap::new() - }, UpdatesAction::Add); - } - match tracker.save_user_updates(tracker.clone()).await { - Ok(_) => {} - Err(_) => { - panic!("[IMPORT] Unable to save users to the database!"); - } - } - } - Err(error) => { - error!("[IMPORT] The users file {} could not be imported!", args.import_file_users.as_str()); + if config.users_enabled { + let users_file = &args.import_file_users; + info!("[IMPORT] Importing users to memory {users_file}"); + + let data = fs::read(users_file) + .unwrap_or_else(|error| { + error!("[IMPORT] The users file {users_file} could not be imported!"); panic!("[IMPORT] {error}") - } + }); + + let users: Value = serde_json::from_slice(&data) + .expect("[IMPORT] Failed to parse users JSON"); + + let users_obj = users.as_object() + .expect("[IMPORT] Users data is not a JSON object"); + + for (key, value) in users_obj { + let user_hash_bytes = hex::decode(key) + .expect("[IMPORT] User hash is not hex or invalid!"); + + let user_hash = UserId(user_hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + let key_str = value["key"].as_str() + .expect("[IMPORT] Key field is missing or not a string"); + + let key_hash_bytes = hex::decode(key_str) + .expect("[IMPORT] Key hash is not hex or invalid!"); + + let key_hash = UserId(key_hash_bytes[..20].try_into() + .expect("[IMPORT] Invalid hash length")); + + let user_entry = UserEntryItem { + key: key_hash, + user_id: value["user_id"].as_u64(), + user_uuid: value["user_uuid"].as_str().map(String::from), + uploaded: value["uploaded"].as_u64() + .expect("[IMPORT] 'uploaded' field doesn't exist or is missing!"), + downloaded: value["downloaded"].as_u64() + .expect("[IMPORT] 'downloaded' field doesn't exist or is missing!"), + completed: value["completed"].as_u64() + .expect("[IMPORT] 'completed' field doesn't exist or is missing!"), + updated: value["updated"].as_u64() + .expect("[IMPORT] 'updated' field doesn't exist or is missing!"), + active: value["active"].as_u64() + .expect("[IMPORT] 'active' field doesn't exist or is missing!") as u8, + torrents_active: BTreeMap::new() + }; + + tracker.add_user_update(user_hash, user_entry, UpdatesAction::Add); } + + tracker.save_user_updates(Arc::clone(&tracker)).await + .expect("[IMPORT] Unable to save users to the database!"); } info!("[IMPORT] Importing of data completed"); diff --git a/src/tracker/impls/torrent_tracker_keys.rs b/src/tracker/impls/torrent_tracker_keys.rs index 7f8f04d..402ef6f 100644 --- a/src/tracker/impls/torrent_tracker_keys.rs +++ b/src/tracker/impls/torrent_tracker_keys.rs @@ -13,7 +13,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn load_keys(&self, tracker: Arc) { - if let Ok(keys) = self.sqlx.load_keys(tracker.clone()).await { + if let Ok(keys) = self.sqlx.load_keys(tracker).await { info!("Loaded {keys} keys"); } } @@ -21,13 +21,13 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_keys(&self, tracker: Arc, keys: BTreeMap) -> Result<(), ()> { - match self.sqlx.save_keys(tracker.clone(), keys.clone()).await { + match self.sqlx.save_keys(tracker, keys).await { Ok(keys_count) => { info!("[SYNC KEYS] Synced {keys_count} keys"); Ok(()) } Err(_) => { - error!("[SYNC KEYS] Unable to sync {} keys", keys.len()); + error!("[SYNC KEYS] Unable to sync keys"); Err(()) } } @@ -36,10 +36,10 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_key(&self, hash: InfoHash, timeout: i64) -> bool { - let map = self.keys.clone(); - let mut lock = map.write(); + let mut lock = self.keys.write(); let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let timeout_unix = timestamp.as_secs() as i64 + timeout; + match lock.entry(hash) { Entry::Vacant(v) => { self.update_stats(StatsEvent::Key, 1); @@ -56,63 +56,47 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_key(&self, hash: InfoHash) -> Option<(InfoHash, i64)> { - let map = self.keys.clone(); - let lock = map.read_recursive(); - lock.get(&hash).map(|data| (hash, *data)) + let lock = self.keys.read_recursive(); + lock.get(&hash).map(|&data| (hash, data)) } #[tracing::instrument(level = "debug")] pub fn get_keys(&self) -> BTreeMap { - let map = self.keys.clone(); - let lock = map.read_recursive(); + let lock = self.keys.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_key(&self, hash: InfoHash) -> bool { - let map = self.keys.clone(); - let mut lock = map.write(); - match lock.remove(&hash) { - None => { - false - } - Some(_) => { - self.update_stats(StatsEvent::Key, -1); - true - } + let mut lock = self.keys.write(); + if lock.remove(&hash).is_some() { + self.update_stats(StatsEvent::Key, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn check_key(&self, hash: InfoHash) -> bool { - let map = self.keys.clone(); - let lock = map.read_recursive(); - match lock.get(&hash) { - None => { - false - } - Some(key) => { - let time = SystemTime::from(Utc.timestamp_opt(*key, 0).unwrap()); - match time.duration_since(SystemTime::now()) { - Ok(_) => { - true - } - Err(_) => { - false - } - } - } - } + let lock = self.keys.read_recursive(); + lock.get(&hash).is_some_and(|&key| { + let key_time = Utc.timestamp_opt(key, 0) + .single() + .map(SystemTime::from) + .unwrap_or(UNIX_EPOCH); + + key_time > SystemTime::now() + }) } #[tracing::instrument(level = "debug")] pub fn clear_keys(&self) { - let map = self.keys.clone(); - let mut lock = map.write(); + let mut lock = self.keys.write(); lock.clear(); self.set_stats(StatsEvent::Key, 0); } @@ -120,12 +104,25 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn clean_keys(&self) { - let keys = self.get_keys(); - for (hash, key_time) in keys.iter() { - let time = SystemTime::from(Utc.timestamp_opt(*key_time, 0).unwrap()); - if time.duration_since(SystemTime::now()).is_err() { - self.remove_key(*hash); + let now = SystemTime::now(); + let mut keys_to_remove = Vec::new(); + + { + let lock = self.keys.read_recursive(); + for (&hash, &key_time) in lock.iter() { + let time = Utc.timestamp_opt(key_time, 0) + .single() + .map(SystemTime::from) + .unwrap_or(UNIX_EPOCH); + + if time <= now { + keys_to_remove.push(hash); + } } } + + for hash in keys_to_remove { + self.remove_key(hash); + } } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_keys_updates.rs b/src/tracker/impls/torrent_tracker_keys_updates.rs index 40a7301..891593b 100644 --- a/src/tracker/impls/torrent_tracker_keys_updates.rs +++ b/src/tracker/impls/torrent_tracker_keys_updates.rs @@ -12,46 +12,40 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_key_update(&self, info_hash: InfoHash, timeout: i64, updates_action: UpdatesAction) -> bool { - let map = self.keys_updates.clone(); - let mut lock = map.write(); - match lock.insert(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(), (info_hash, timeout, updates_action)) { - None => { - self.update_stats(StatsEvent::KeyUpdates, 1); - true - } - Some(_) => { - false - } + let mut lock = self.keys_updates.write(); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + + if lock.insert(timestamp, (info_hash, timeout, updates_action)).is_none() { + self.update_stats(StatsEvent::KeyUpdates, 1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn get_key_updates(&self) -> HashMap { - let map = self.keys_updates.clone(); - let lock = map.read_recursive(); + let lock = self.keys_updates.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_key_update(&self, timestamp: &u128) -> bool { - let map = self.keys_updates.clone(); - let mut lock = map.write(); - match lock.remove(timestamp) { - None => { false } - Some(_) => { - self.update_stats(StatsEvent::KeyUpdates, -1); - true - } + let mut lock = self.keys_updates.write(); + if lock.remove(timestamp).is_some() { + self.update_stats(StatsEvent::KeyUpdates, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_key_updates(&self) { - let map = self.keys_updates.clone(); - let mut lock = map.write(); + let mut lock = self.keys_updates.write(); lock.clear(); self.set_stats(StatsEvent::KeyUpdates, 0); } @@ -59,26 +53,45 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_key_updates(&self, torrent_tracker: Arc) -> Result<(), ()> { + let updates = self.get_key_updates(); + let mut mapping: HashMap = HashMap::new(); - for (timestamp, (info_hash, timeout, updates_action)) in self.get_key_updates().iter() { - match mapping.entry(*info_hash) { + let mut timestamps_to_remove = Vec::new(); + + for (timestamp, (info_hash, timeout, updates_action)) in updates { + match mapping.entry(info_hash) { Entry::Occupied(mut o) => { - o.insert((o.get().0, *timeout, *updates_action)); - self.remove_key_update(timestamp); + let existing = o.get(); + if timestamp > existing.0 { + timestamps_to_remove.push(existing.0); + o.insert((timestamp, timeout, updates_action)); + } else { + timestamps_to_remove.push(timestamp); + } } Entry::Vacant(v) => { - v.insert((*timestamp, *timeout, *updates_action)); + v.insert((timestamp, timeout, updates_action)); } } } - match self.save_keys(torrent_tracker.clone(), mapping.clone().into_iter().map(|(info_hash, (_, timeout, updates_action))| { - (info_hash, (timeout, updates_action)) - }).collect::>()).await { + + let keys_to_save: BTreeMap = mapping + .iter() + .map(|(info_hash, (_, timeout, updates_action))| (*info_hash, (*timeout, *updates_action))) + .collect(); + + match self.save_keys(torrent_tracker, keys_to_save).await { Ok(_) => { info!("[SYNC KEY UPDATES] Synced {} keys", mapping.len()); - for (_, (timestamp, _, _)) in mapping.into_iter() { + + for (_, (timestamp, _, _)) in mapping { + self.remove_key_update(×tamp); + } + + for timestamp in timestamps_to_remove { self.remove_key_update(×tamp); } + Ok(()) } Err(_) => { diff --git a/src/tracker/impls/torrent_tracker_peers.rs b/src/tracker/impls/torrent_tracker_peers.rs index ff43745..8494534 100644 --- a/src/tracker/impls/torrent_tracker_peers.rs +++ b/src/tracker/impls/torrent_tracker_peers.rs @@ -16,183 +16,128 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_torrent_peers(&self, info_hash: InfoHash, amount: usize, ip_type: TorrentPeersType, self_ip: Option) -> Option { - let mut returned_data = TorrentPeers { - seeds_ipv4: BTreeMap::new(), - seeds_ipv6: BTreeMap::new(), - peers_ipv4: BTreeMap::new(), - peers_ipv6: BTreeMap::new() - }; - match self.get_torrent(info_hash) { - None => { None } - Some(data) => { - match ip_type { - TorrentPeersType::All => { - returned_data.seeds_ipv4 = self.get_peers(data.seeds.clone(), TorrentPeersType::IPv4, self_ip, amount).unwrap_or_default(); - returned_data.seeds_ipv6 = self.get_peers(data.seeds.clone(), TorrentPeersType::IPv6, self_ip, amount).unwrap_or_default(); - returned_data.peers_ipv4 = self.get_peers(data.peers.clone(), TorrentPeersType::IPv4, self_ip, amount).unwrap_or_default(); - returned_data.peers_ipv6 = self.get_peers(data.peers.clone(), TorrentPeersType::IPv6, self_ip, amount).unwrap_or_default(); - } - TorrentPeersType::IPv4 => { - returned_data.seeds_ipv4 = self.get_peers(data.seeds.clone(), TorrentPeersType::IPv4, self_ip, amount).unwrap_or_default(); - returned_data.peers_ipv4 = self.get_peers(data.peers.clone(), TorrentPeersType::IPv4, self_ip, amount).unwrap_or_default(); - } - TorrentPeersType::IPv6 => { - returned_data.seeds_ipv6 = self.get_peers(data.seeds.clone(), TorrentPeersType::IPv6, self_ip, amount).unwrap_or_default(); - returned_data.peers_ipv6 = self.get_peers(data.peers.clone(), TorrentPeersType::IPv6, self_ip, amount).unwrap_or_default(); - } + self.get_torrent(info_hash).map(|data| { + let mut returned_data = TorrentPeers { + seeds_ipv4: BTreeMap::new(), + seeds_ipv6: BTreeMap::new(), + peers_ipv4: BTreeMap::new(), + peers_ipv6: BTreeMap::new() + }; + + match ip_type { + TorrentPeersType::All => { + returned_data.seeds_ipv4 = self.get_peers(&data.seeds, TorrentPeersType::IPv4, self_ip, amount); + returned_data.seeds_ipv6 = self.get_peers(&data.seeds, TorrentPeersType::IPv6, self_ip, amount); + returned_data.peers_ipv4 = self.get_peers(&data.peers, TorrentPeersType::IPv4, self_ip, amount); + returned_data.peers_ipv6 = self.get_peers(&data.peers, TorrentPeersType::IPv6, self_ip, amount); + } + TorrentPeersType::IPv4 => { + returned_data.seeds_ipv4 = self.get_peers(&data.seeds, TorrentPeersType::IPv4, self_ip, amount); + returned_data.peers_ipv4 = self.get_peers(&data.peers, TorrentPeersType::IPv4, self_ip, amount); + } + TorrentPeersType::IPv6 => { + returned_data.seeds_ipv6 = self.get_peers(&data.seeds, TorrentPeersType::IPv6, self_ip, amount); + returned_data.peers_ipv6 = self.get_peers(&data.peers, TorrentPeersType::IPv6, self_ip, amount); } - Some(returned_data) } - } + + returned_data + }) } #[tracing::instrument(level = "debug")] - pub fn get_peers(&self, peers: BTreeMap, type_ip: TorrentPeersType, self_ip: Option, amount: usize) -> Option> + pub fn get_peers(&self, peers: &BTreeMap, type_ip: TorrentPeersType, self_ip: Option, amount: usize) -> BTreeMap { - if amount != 0 { - return peers.iter().take(amount).map(|(peer_id, torrent_peer)| { - match type_ip { - TorrentPeersType::All => { None } - TorrentPeersType::IPv4 => { - match self_ip { - None => { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { Some((*peer_id, torrent_peer.clone())) } - SocketAddr::V6(_) => { None} - } - } - Some(ip) => { - if ip != torrent_peer.peer_addr.ip() { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { Some((*peer_id, torrent_peer.clone())) } - SocketAddr::V6(_) => { None } - } - } else { - None - } - } - } - } - TorrentPeersType::IPv6 => { - match self_ip { - None => { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { None } - SocketAddr::V6(_) => { Some((*peer_id, torrent_peer.clone())) } - } - } - Some(ip) => { - if ip != torrent_peer.peer_addr.ip() { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { None } - SocketAddr::V6(_) => { Some((*peer_id, torrent_peer.clone())) } - } - } else { - None - } - } - } - } + let should_include = |peer_addr: &SocketAddr| -> bool { + let ip_type_match = match type_ip { + TorrentPeersType::All => return false, + TorrentPeersType::IPv4 => peer_addr.is_ipv4(), + TorrentPeersType::IPv6 => peer_addr.is_ipv6(), + }; + + ip_type_match && self_ip.is_none_or(|ip| ip != peer_addr.ip()) + }; + + let iter = peers.iter() + .filter_map(|(peer_id, torrent_peer)| { + if should_include(&torrent_peer.peer_addr) { + Some((*peer_id, torrent_peer.clone())) + } else { + None } - }).collect(); + }); + + if amount != 0 { + iter.take(amount).collect() + } else { + iter.collect() } - peers.iter().map(|(peer_id, torrent_peer)| { - match type_ip { - TorrentPeersType::All => { None } - TorrentPeersType::IPv4 => { - match self_ip { - None => { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { Some((*peer_id, torrent_peer.clone())) } - SocketAddr::V6(_) => { None} - } - } - Some(ip) => { - if ip != torrent_peer.peer_addr.ip() { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { Some((*peer_id, torrent_peer.clone())) } - SocketAddr::V6(_) => { None } - } - } else { - None - } - } - } - } - TorrentPeersType::IPv6 => { - match self_ip { - None => { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { None } - SocketAddr::V6(_) => { Some((*peer_id, torrent_peer.clone())) } - } - } - Some(ip) => { - if ip != torrent_peer.peer_addr.ip() { - match torrent_peer.peer_addr { - SocketAddr::V4(_) => { None } - SocketAddr::V6(_) => { Some((*peer_id, torrent_peer.clone())) } - } - } else { - None - } - } - } - } - } - }).collect() } #[tracing::instrument(level = "debug")] pub fn add_torrent_peer(&self, info_hash: InfoHash, peer_id: PeerId, torrent_peer: TorrentPeer, completed: bool) -> (Option, TorrentEntry) { - let shard = self.torrents_sharding.clone().get_shard(info_hash.0[0]).unwrap(); + let shard = self.torrents_sharding.get_shard(info_hash.0[0]).unwrap(); let mut lock = shard.write(); + match lock.entry(info_hash) { Entry::Vacant(v) => { let mut torrent_entry = TorrentEntry { seeds: BTreeMap::new(), peers: BTreeMap::new(), - completed: if completed && torrent_peer.left == NumberOfBytes(0) { self.update_stats(StatsEvent::Completed, 1); 1 } else { 0 }, + completed: 0, updated: std::time::Instant::now() }; + + if completed && torrent_peer.left == NumberOfBytes(0) { + self.update_stats(StatsEvent::Completed, 1); + torrent_entry.completed = 1; + } + self.update_stats(StatsEvent::Torrents, 1); - match torrent_peer.left { - NumberOfBytes(0) => { - self.update_stats(StatsEvent::Seeds, 1); - torrent_entry.seeds.insert(peer_id, torrent_peer); - } - _ => { - self.update_stats(StatsEvent::Peers, 1); - torrent_entry.peers.insert(peer_id, torrent_peer); - } + + if torrent_peer.left == NumberOfBytes(0) { + self.update_stats(StatsEvent::Seeds, 1); + torrent_entry.seeds.insert(peer_id, torrent_peer); + } else { + self.update_stats(StatsEvent::Peers, 1); + torrent_entry.peers.insert(peer_id, torrent_peer); } - v.insert(torrent_entry.clone()); - (None, torrent_entry) + + let entry_clone = torrent_entry.clone(); + v.insert(torrent_entry); + (None, entry_clone) } Entry::Occupied(mut o) => { let previous_torrent = o.get().clone(); - if o.get_mut().seeds.remove(&peer_id).is_some() { + let entry = o.get_mut(); + + let was_seed = entry.seeds.remove(&peer_id).is_some(); + let was_peer = entry.peers.remove(&peer_id).is_some(); + + if was_seed { self.update_stats(StatsEvent::Seeds, -1); - }; - if o.get_mut().peers.remove(&peer_id).is_some() { + } + if was_peer { self.update_stats(StatsEvent::Peers, -1); - }; + } + if completed { self.update_stats(StatsEvent::Completed, 1); - o.get_mut().completed += 1; + entry.completed += 1; } - match torrent_peer.left { - NumberOfBytes(0) => { - self.update_stats(StatsEvent::Seeds, 1); - o.get_mut().seeds.insert(peer_id, torrent_peer); - } - _ => { - self.update_stats(StatsEvent::Peers, 1); - o.get_mut().peers.insert(peer_id, torrent_peer); - } + + if torrent_peer.left == NumberOfBytes(0) { + self.update_stats(StatsEvent::Seeds, 1); + entry.seeds.insert(peer_id, torrent_peer); + } else { + self.update_stats(StatsEvent::Peers, 1); + entry.peers.insert(peer_id, torrent_peer); } - (Some(previous_torrent), o.get().clone()) + + entry.updated = std::time::Instant::now(); + + (Some(previous_torrent), entry.clone()) } } } @@ -200,30 +145,40 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn remove_torrent_peer(&self, info_hash: InfoHash, peer_id: PeerId, persistent: bool, cleanup: bool) -> (Option, Option) { - if !self.torrents_sharding.contains_peer(info_hash, peer_id) { return (None, None); } - let shard = self.torrents_sharding.clone().get_shard(info_hash.0[0]).unwrap(); + if !self.torrents_sharding.contains_peer(info_hash, peer_id) { + return (None, None); + } + + let shard = self.torrents_sharding.get_shard(info_hash.0[0]).unwrap(); let mut lock = shard.write(); + match lock.entry(info_hash) { - Entry::Vacant(_) => { - (None, None) - } + Entry::Vacant(_) => (None, None), Entry::Occupied(mut o) => { if cleanup { info!("[PEERS] Removing from torrent {info_hash} peer {peer_id}"); } + let previous_torrent = o.get().clone(); - if o.get_mut().seeds.remove(&peer_id).is_some() { + let entry = o.get_mut(); + + let was_seed = entry.seeds.remove(&peer_id).is_some(); + let was_peer = entry.peers.remove(&peer_id).is_some(); + + if was_seed { self.update_stats(StatsEvent::Seeds, -1); - }; - if o.get_mut().peers.remove(&peer_id).is_some() { + } + if was_peer { self.update_stats(StatsEvent::Peers, -1); - }; - if !persistent && o.get().seeds.is_empty() && o.get().peers.is_empty() { - lock.remove(&info_hash); + } + + if !persistent && entry.seeds.is_empty() && entry.peers.is_empty() { + o.remove(); self.update_stats(StatsEvent::Torrents, -1); - return (Some(previous_torrent), None); + (Some(previous_torrent), None) + } else { + (Some(previous_torrent), Some(entry.clone())) } - (Some(previous_torrent), Some(o.get().clone())) } } } diff --git a/src/tracker/impls/torrent_tracker_torrents.rs b/src/tracker/impls/torrent_tracker_torrents.rs index 7ff0af9..c820757 100644 --- a/src/tracker/impls/torrent_tracker_torrents.rs +++ b/src/tracker/impls/torrent_tracker_torrents.rs @@ -12,7 +12,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn load_torrents(&self, tracker: Arc) { - if let Ok((torrents, completes)) = self.sqlx.load_torrents(tracker.clone()).await { + if let Ok((torrents, completes)) = self.sqlx.load_torrents(tracker).await { info!("Loaded {torrents} torrents with {completes} completes"); } } @@ -20,13 +20,14 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_torrents(&self, tracker: Arc, torrents: BTreeMap) -> Result<(), ()> { - match self.sqlx.save_torrents(tracker.clone(), torrents.clone()).await { + let torrents_count = torrents.len(); + match self.sqlx.save_torrents(tracker, torrents).await { Ok(_) => { - info!("[SYNC TORRENTS] Synced {} torrents", torrents.len()); + info!("[SYNC TORRENTS] Synced {torrents_count} torrents"); Ok(()) } Err(_) => { - error!("[SYNC TORRENTS] Unable to sync {} torrents", torrents.len()); + error!("[SYNC TORRENTS] Unable to sync {torrents_count} torrents"); Err(()) } } @@ -35,7 +36,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn reset_seeds_peers(&self, tracker: Arc) -> bool { - match self.sqlx.reset_seeds_peers(tracker.clone()).await { + match self.sqlx.reset_seeds_peers(tracker).await { Ok(_) => { info!("[RESET SEEDS PEERS] Completed"); true @@ -50,28 +51,43 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_torrent(&self, info_hash: InfoHash, torrent_entry: TorrentEntry) -> (TorrentEntry, bool) { - let shard = self.torrents_sharding.clone().get_shard(info_hash.0[0]).unwrap(); + let shard = self.torrents_sharding.get_shard(info_hash.0[0]).unwrap(); let mut lock = shard.write(); + match lock.entry(info_hash) { Entry::Vacant(v) => { self.update_stats(StatsEvent::Torrents, 1); self.update_stats(StatsEvent::Completed, torrent_entry.completed as i64); self.update_stats(StatsEvent::Seeds, torrent_entry.seeds.len() as i64); self.update_stats(StatsEvent::Peers, torrent_entry.peers.len() as i64); - (v.insert(torrent_entry).clone(), true) + + let entry_clone = torrent_entry.clone(); + v.insert(torrent_entry); + (entry_clone, true) } Entry::Occupied(mut o) => { - self.update_stats(StatsEvent::Completed, 0i64 - o.get().completed as i64); - self.update_stats(StatsEvent::Completed, torrent_entry.completed as i64); - o.get_mut().completed = torrent_entry.completed; - self.update_stats(StatsEvent::Seeds, 0i64 - o.get().seeds.len() as i64); - self.update_stats(StatsEvent::Seeds, torrent_entry.seeds.len() as i64); - o.get_mut().seeds = torrent_entry.seeds.clone(); - self.update_stats(StatsEvent::Peers, 0i64 - o.get().peers.len() as i64); - self.update_stats(StatsEvent::Peers, torrent_entry.peers.len() as i64); - o.get_mut().peers = torrent_entry.peers.clone(); - o.get_mut().updated = torrent_entry.updated; - (torrent_entry.clone(), false) + let current = o.get_mut(); + + let completed_delta = torrent_entry.completed as i64 - current.completed as i64; + let seeds_delta = torrent_entry.seeds.len() as i64 - current.seeds.len() as i64; + let peers_delta = torrent_entry.peers.len() as i64 - current.peers.len() as i64; + + if completed_delta != 0 { + self.update_stats(StatsEvent::Completed, completed_delta); + } + if seeds_delta != 0 { + self.update_stats(StatsEvent::Seeds, seeds_delta); + } + if peers_delta != 0 { + self.update_stats(StatsEvent::Peers, peers_delta); + } + + current.completed = torrent_entry.completed; + current.seeds = torrent_entry.seeds; + current.peers = torrent_entry.peers; + current.updated = torrent_entry.updated; + + (current.clone(), false) } } } @@ -79,68 +95,61 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_torrents(&self, hashes: BTreeMap) -> BTreeMap { - let mut returned_data = BTreeMap::new(); - for (info_hash, torrent_entry) in hashes.iter() { - returned_data.insert(*info_hash, self.add_torrent(*info_hash, torrent_entry.clone())); - } - returned_data + hashes.into_iter() + .map(|(info_hash, torrent_entry)| { + let result = self.add_torrent(info_hash, torrent_entry); + (info_hash, result) + }) + .collect() } #[tracing::instrument(level = "debug")] pub fn get_torrent(&self, info_hash: InfoHash) -> Option { - let shard = self.torrents_sharding.clone().get_shard(info_hash.0[0]).unwrap(); + let shard = self.torrents_sharding.get_shard(info_hash.0[0]).unwrap(); let lock = shard.read_recursive(); - lock.get(&info_hash).map(|torrent| TorrentEntry { - seeds: torrent.seeds.clone(), - peers: torrent.peers.clone(), - completed: torrent.completed, - updated: torrent.updated - }) + lock.get(&info_hash).cloned() } #[tracing::instrument(level = "debug")] pub fn get_torrents(&self, hashes: Vec) -> BTreeMap> { - let mut returned_data = BTreeMap::new(); - for info_hash in hashes.iter() { - returned_data.insert(*info_hash, self.get_torrent(*info_hash)); - } - returned_data + hashes.into_iter() + .map(|info_hash| { + let entry = self.get_torrent(info_hash); + (info_hash, entry) + }) + .collect() } #[tracing::instrument(level = "debug")] pub fn remove_torrent(&self, info_hash: InfoHash) -> Option { - if !self.torrents_sharding.contains_torrent(info_hash) { return None; } - let shard = self.torrents_sharding.clone().get_shard(info_hash.0[0]).unwrap(); + if !self.torrents_sharding.contains_torrent(info_hash) { + return None; + } + + let shard = self.torrents_sharding.get_shard(info_hash.0[0]).unwrap(); let mut lock = shard.write(); - match lock.remove(&info_hash) { - None => { None } - Some(data) => { - self.update_stats(StatsEvent::Torrents, -1); - self.update_stats(StatsEvent::Seeds, data.seeds.len() as i64); - self.update_stats(StatsEvent::Peers, data.peers.len() as i64); - Some(data) - } + + if let Some(data) = lock.remove(&info_hash) { + self.update_stats(StatsEvent::Torrents, -1); + self.update_stats(StatsEvent::Seeds, -(data.seeds.len() as i64)); + self.update_stats(StatsEvent::Peers, -(data.peers.len() as i64)); + Some(data) + } else { + None } } #[tracing::instrument(level = "debug")] pub fn remove_torrents(&self, hashes: Vec) -> BTreeMap> { - let mut returned_data = BTreeMap::new(); - for info_hash in hashes.iter() { - returned_data.insert(*info_hash, match self.remove_torrent(*info_hash) { - None => { None } - Some(torrent) => { - self.update_stats(StatsEvent::Torrents, -1); - self.update_stats(StatsEvent::Seeds, torrent.seeds.len() as i64); - self.update_stats(StatsEvent::Peers, torrent.peers.len() as i64); - Some(torrent) - } - }); - } - returned_data + hashes.into_iter() + .map(|info_hash| { + let result = self.remove_torrent(info_hash); + (info_hash, result) + }) + .collect() } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_torrents_blacklist.rs b/src/tracker/impls/torrent_tracker_torrents_blacklist.rs index e1ff47d..7464732 100644 --- a/src/tracker/impls/torrent_tracker_torrents_blacklist.rs +++ b/src/tracker/impls/torrent_tracker_torrents_blacklist.rs @@ -9,7 +9,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn load_blacklist(&self, tracker: Arc) { - if let Ok(blacklist) = self.sqlx.load_blacklist(tracker.clone()).await { + if let Ok(blacklist) = self.sqlx.load_blacklist(tracker).await { info!("Loaded {blacklist} blacklists"); } } @@ -17,13 +17,14 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_blacklist(&self, tracker: Arc, hashes: Vec<(InfoHash, UpdatesAction)>) -> Result<(), ()> { - match self.sqlx.save_blacklist(tracker.clone(), hashes.clone()).await { + let hashes_len = hashes.len(); + match self.sqlx.save_blacklist(tracker, hashes).await { Ok(_) => { - info!("[SYNC BLACKLIST] Synced {} blacklists", hashes.len()); + info!("[SYNC BLACKLIST] Synced {hashes_len} blacklists"); Ok(()) } Err(_) => { - error!("[SYNC BLACKLIST] Unable to sync {} blacklists", hashes.len()); + error!("[SYNC BLACKLIST] Unable to sync {hashes_len} blacklists"); Err(()) } } @@ -32,8 +33,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_blacklist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_blacklist.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_blacklist.write(); if !lock.contains(&info_hash) { lock.push(info_hash); self.update_stats(StatsEvent::Blacklist, 1); @@ -41,46 +41,38 @@ impl TorrentTracker { } false } - + #[tracing::instrument(level = "debug")] pub fn get_blacklist(&self) -> Vec { - let map = self.torrents_blacklist.clone(); - let lock = map.read_recursive(); + let lock = self.torrents_blacklist.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn check_blacklist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_blacklist.clone(); - let lock = map.read_recursive(); - if lock.contains(&info_hash) { - return true; - } - false + let lock = self.torrents_blacklist.read_recursive(); + lock.contains(&info_hash) } #[tracing::instrument(level = "debug")] pub fn remove_blacklist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_blacklist.clone(); - let mut lock = map.write(); - match lock.iter().position(|r| *r == info_hash) { - None => { false } - Some(index) => { - lock.remove(index); - self.update_stats(StatsEvent::Blacklist, -1); - true - } + let mut lock = self.torrents_blacklist.write(); + if let Some(index) = lock.iter().position(|r| *r == info_hash) { + lock.swap_remove(index); + self.update_stats(StatsEvent::Blacklist, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_blacklist(&self) { - let map = self.torrents_blacklist.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_blacklist.write(); lock.clear(); } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_torrents_blacklist_updates.rs b/src/tracker/impls/torrent_tracker_torrents_blacklist_updates.rs index f2855b3..509bc74 100644 --- a/src/tracker/impls/torrent_tracker_torrents_blacklist_updates.rs +++ b/src/tracker/impls/torrent_tracker_torrents_blacklist_updates.rs @@ -12,25 +12,35 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_blacklist_update(&self, info_hash: InfoHash, updates_action: UpdatesAction) -> bool { - let map = self.torrents_blacklist_updates.clone(); - let mut lock = map.write(); - match lock.insert(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(), (info_hash, updates_action)) { - None => { - self.update_stats(StatsEvent::BlacklistUpdates, 1); - true - } - Some(_) => { - false - } + let mut lock = self.torrents_blacklist_updates.write(); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + + if lock.insert(timestamp, (info_hash, updates_action)).is_none() { + self.update_stats(StatsEvent::BlacklistUpdates, 1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn add_blacklist_updates(&self, hashes: Vec<(InfoHash, UpdatesAction)>) -> Vec<(InfoHash, bool)> { - let mut returned_data = Vec::new(); + let mut lock = self.torrents_blacklist_updates.write(); + let mut returned_data = Vec::with_capacity(hashes.len()); + let mut success_count = 0i64; + for (info_hash, updates_action) in hashes { - returned_data.push((info_hash, self.add_blacklist_update(info_hash, updates_action))); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + let success = lock.insert(timestamp, (info_hash, updates_action)).is_none(); + if success { + success_count += 1; + } + returned_data.push((info_hash, success)); + } + + if success_count > 0 { + self.update_stats(StatsEvent::BlacklistUpdates, success_count); } returned_data } @@ -38,30 +48,26 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_blacklist_updates(&self) -> HashMap { - let map = self.torrents_blacklist_updates.clone(); - let lock = map.read_recursive(); + let lock = self.torrents_blacklist_updates.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_blacklist_update(&self, timestamp: &u128) -> bool { - let map = self.torrents_blacklist_updates.clone(); - let mut lock = map.write(); - match lock.remove(timestamp) { - None => { false } - Some(_) => { - self.update_stats(StatsEvent::BlacklistUpdates, -1); - true - } + let mut lock = self.torrents_blacklist_updates.write(); + if lock.remove(timestamp).is_some() { + self.update_stats(StatsEvent::BlacklistUpdates, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_blacklist_updates(&self) { - let map = self.torrents_blacklist_updates.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_blacklist_updates.write(); lock.clear(); self.set_stats(StatsEvent::BlacklistUpdates, 0); } @@ -69,30 +75,68 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_blacklist_updates(&self, torrent_tracker: Arc) -> Result<(), ()> { - let mut mapping: HashMap = HashMap::new(); - for (timestamp, (info_hash, updates_action)) in self.get_blacklist_updates().iter() { - match mapping.entry(*info_hash) { + let updates = { + let lock = self.torrents_blacklist_updates.read_recursive(); + lock.clone() + }; + + if updates.is_empty() { + return Ok(()); + } + + let mut mapping: HashMap = HashMap::with_capacity(updates.len()); + let mut timestamps_to_remove = Vec::new(); + + for (timestamp, (info_hash, updates_action)) in updates { + match mapping.entry(info_hash) { Entry::Occupied(mut o) => { - o.insert((o.get().0, *updates_action)); - self.remove_blacklist_update(timestamp); + let existing = o.get(); + if timestamp > existing.0 { + timestamps_to_remove.push(existing.0); + o.insert((timestamp, updates_action)); + } else { + timestamps_to_remove.push(timestamp); + } } Entry::Vacant(v) => { - v.insert((*timestamp, *updates_action)); + v.insert((timestamp, updates_action)); } } } - match self.save_blacklist(torrent_tracker.clone(), mapping.clone().into_iter().map(|(info_hash, (_, updates_action))| { - (info_hash, updates_action) - }).collect::>()).await { + + let mapping_len = mapping.len(); + let blacklist_updates: Vec<(InfoHash, UpdatesAction)> = mapping + .iter() + .map(|(info_hash, (_, updates_action))| (*info_hash, *updates_action)) + .collect(); + + match self.save_blacklist(torrent_tracker, blacklist_updates).await { Ok(_) => { - info!("[SYNC BLACKLIST UPDATES] Synced {} blacklists", mapping.len()); - for (_, (timestamp, _)) in mapping.into_iter() { - self.remove_blacklist_update(×tamp); + info!("[SYNC BLACKLIST UPDATES] Synced {mapping_len} blacklists"); + + let mut lock = self.torrents_blacklist_updates.write(); + let mut removed_count = 0i64; + + for (_, (timestamp, _)) in mapping { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } } + + for timestamp in timestamps_to_remove { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } + } + + if removed_count > 0 { + self.update_stats(StatsEvent::BlacklistUpdates, -removed_count); + } + Ok(()) } Err(_) => { - error!("[SYNC BLACKLIST UPDATES] Unable to sync {} blacklists", mapping.len()); + error!("[SYNC BLACKLIST UPDATES] Unable to sync {mapping_len} blacklists"); Err(()) } } diff --git a/src/tracker/impls/torrent_tracker_torrents_updates.rs b/src/tracker/impls/torrent_tracker_torrents_updates.rs index 9168874..3f1e3ff 100644 --- a/src/tracker/impls/torrent_tracker_torrents_updates.rs +++ b/src/tracker/impls/torrent_tracker_torrents_updates.rs @@ -13,57 +13,71 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_torrent_update(&self, info_hash: InfoHash, torrent_entry: TorrentEntry, updates_action: UpdatesAction) -> bool { - let map = self.torrents_updates.clone(); - let mut lock = map.write(); - match lock.insert(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(), (info_hash, torrent_entry.clone(), updates_action)) { - None => { - self.update_stats(StatsEvent::TorrentsUpdates, 1); - true - } - Some(_) => { - false - } + let mut lock = self.torrents_updates.write(); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + + if lock.insert(timestamp, (info_hash, torrent_entry, updates_action)).is_none() { + self.update_stats(StatsEvent::TorrentsUpdates, 1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn add_torrent_updates(&self, hashes: HashMap) -> BTreeMap { + let mut lock = self.torrents_updates.write(); let mut returned_data = BTreeMap::new(); - for (timestamp, (info_hash, torrent_entry, updates_action)) in hashes.iter() { - returned_data.insert(*info_hash, self.add_torrent_update(*info_hash, torrent_entry.clone(), *updates_action)); - let _ = self.remove_torrent_update(timestamp); + let mut success_count = 0i64; + let mut remove_count = 0i64; + + for (timestamp, (info_hash, torrent_entry, updates_action)) in hashes { + let new_timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + let success = lock.insert(new_timestamp, (info_hash, torrent_entry, updates_action)).is_none(); + if success { + success_count += 1; + } + returned_data.insert(info_hash, success); + + if lock.remove(×tamp).is_some() { + remove_count += 1; + } + } + + if success_count > 0 { + self.update_stats(StatsEvent::TorrentsUpdates, success_count); } + if remove_count > 0 { + self.update_stats(StatsEvent::TorrentsUpdates, -remove_count); + } + returned_data } #[tracing::instrument(level = "debug")] pub fn get_torrent_updates(&self) -> HashMap { - let map = self.torrents_updates.clone(); - let lock = map.read_recursive(); + let lock = self.torrents_updates.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_torrent_update(&self, timestamp: &u128) -> bool { - let map = self.torrents_updates.clone(); - let mut lock = map.write(); - match lock.remove(timestamp) { - None => { false } - Some(_) => { - self.update_stats(StatsEvent::TorrentsUpdates, -1); - true - } + let mut lock = self.torrents_updates.write(); + if lock.remove(timestamp).is_some() { + self.update_stats(StatsEvent::TorrentsUpdates, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_torrent_updates(&self) { - let map = self.torrents_updates.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_updates.write(); lock.clear(); self.set_stats(StatsEvent::TorrentsUpdates, 0); } @@ -71,30 +85,68 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_torrent_updates(&self, torrent_tracker: Arc) -> Result<(), ()> { - let mut mapping: HashMap = HashMap::new(); - for (timestamp, (info_hash, torrent_entry, updates_action)) in self.get_torrent_updates().iter() { - match mapping.entry(*info_hash) { + let updates = { + let lock = self.torrents_updates.read_recursive(); + lock.clone() + }; + + if updates.is_empty() { + return Ok(()); + } + + let mut mapping: HashMap = HashMap::with_capacity(updates.len()); + let mut timestamps_to_remove = Vec::new(); + + for (timestamp, (info_hash, torrent_entry, updates_action)) in updates { + match mapping.entry(info_hash) { Entry::Occupied(mut o) => { - o.insert((o.get().0, torrent_entry.clone(), *updates_action)); - self.remove_torrent_update(timestamp); + let existing = o.get(); + if timestamp > existing.0 { + timestamps_to_remove.push(existing.0); + o.insert((timestamp, torrent_entry, updates_action)); + } else { + timestamps_to_remove.push(timestamp); + } } Entry::Vacant(v) => { - v.insert((*timestamp, torrent_entry.clone(), *updates_action)); + v.insert((timestamp, torrent_entry, updates_action)); } } } - match self.save_torrents(torrent_tracker.clone(), mapping.clone().into_iter().map(|(info_hash, (_, torrent_entry, updates_action))| { - (info_hash, (torrent_entry.clone(), updates_action)) - }).collect::>()).await { + + let mapping_len = mapping.len(); + let torrents_to_save: BTreeMap = mapping + .iter() + .map(|(info_hash, (_, torrent_entry, updates_action))| (*info_hash, (torrent_entry.clone(), *updates_action))) + .collect(); + + match self.save_torrents(torrent_tracker, torrents_to_save).await { Ok(_) => { - info!("[SYNC TORRENT UPDATES] Synced {} torrents", mapping.len()); - for (_, (timestamp, _, _)) in mapping.into_iter() { - self.remove_torrent_update(×tamp); + info!("[SYNC TORRENT UPDATES] Synced {mapping_len} torrents"); + + let mut lock = self.torrents_updates.write(); + let mut removed_count = 0i64; + + for (_, (timestamp, _, _)) in mapping { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } } + + for timestamp in timestamps_to_remove { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } + } + + if removed_count > 0 { + self.update_stats(StatsEvent::TorrentsUpdates, -removed_count); + } + Ok(()) } Err(_) => { - error!("[SYNC TORRENT UPDATES] Unable to sync {} torrents", mapping.len()); + error!("[SYNC TORRENT UPDATES] Unable to sync {mapping_len} torrents"); Err(()) } } diff --git a/src/tracker/impls/torrent_tracker_torrents_whitelist.rs b/src/tracker/impls/torrent_tracker_torrents_whitelist.rs index 1821223..709d474 100644 --- a/src/tracker/impls/torrent_tracker_torrents_whitelist.rs +++ b/src/tracker/impls/torrent_tracker_torrents_whitelist.rs @@ -9,7 +9,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn load_whitelist(&self, tracker: Arc) { - if let Ok(whitelist) = self.sqlx.load_whitelist(tracker.clone()).await { + if let Ok(whitelist) = self.sqlx.load_whitelist(tracker).await { info!("Loaded {whitelist} whitelists"); } } @@ -17,13 +17,14 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_whitelist(&self, tracker: Arc, hashes: Vec<(InfoHash, UpdatesAction)>) -> Result<(), ()> { - match self.sqlx.save_whitelist(tracker.clone(), hashes.clone()).await { + let hashes_len = hashes.len(); + match self.sqlx.save_whitelist(tracker, hashes).await { Ok(_) => { - info!("[SYNC WHITELIST] Synced {} whitelists", hashes.len()); + info!("[SYNC WHITELIST] Synced {hashes_len} whitelists"); Ok(()) } Err(_) => { - error!("[SYNC WHITELIST] Unable to sync {} whitelists", hashes.len()); + error!("[SYNC WHITELIST] Unable to sync {hashes_len} whitelists"); Err(()) } } @@ -32,8 +33,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_whitelist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_whitelist.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_whitelist.write(); if !lock.contains(&info_hash) { lock.push(info_hash); self.update_stats(StatsEvent::Whitelist, 1); @@ -45,42 +45,34 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_whitelist(&self) -> Vec { - let map = self.torrents_whitelist.clone(); - let lock = map.read_recursive(); + let lock = self.torrents_whitelist.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn check_whitelist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_whitelist.clone(); - let lock = map.read_recursive(); - if lock.contains(&info_hash) { - return true; - } - false + let lock = self.torrents_whitelist.read_recursive(); + lock.contains(&info_hash) } #[tracing::instrument(level = "debug")] pub fn remove_whitelist(&self, info_hash: InfoHash) -> bool { - let map = self.torrents_whitelist.clone(); - let mut lock = map.write(); - match lock.iter().position(|r| *r == info_hash) { - None => { false } - Some(index) => { - lock.remove(index); - self.update_stats(StatsEvent::Whitelist, -1); - true - } + let mut lock = self.torrents_whitelist.write(); + if let Some(index) = lock.iter().position(|r| *r == info_hash) { + lock.swap_remove(index); + self.update_stats(StatsEvent::Whitelist, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_whitelist(&self) { - let map = self.torrents_whitelist.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_whitelist.write(); lock.clear(); } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_torrents_whitelist_updates.rs b/src/tracker/impls/torrent_tracker_torrents_whitelist_updates.rs index 3f7851d..85e26d3 100644 --- a/src/tracker/impls/torrent_tracker_torrents_whitelist_updates.rs +++ b/src/tracker/impls/torrent_tracker_torrents_whitelist_updates.rs @@ -12,25 +12,35 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_whitelist_update(&self, info_hash: InfoHash, updates_action: UpdatesAction) -> bool { - let map = self.torrents_whitelist_updates.clone(); - let mut lock = map.write(); - match lock.insert(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(), (info_hash, updates_action)) { - None => { - self.update_stats(StatsEvent::WhitelistUpdates, 1); - true - } - Some(_) => { - false - } + let mut lock = self.torrents_whitelist_updates.write(); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + + if lock.insert(timestamp, (info_hash, updates_action)).is_none() { + self.update_stats(StatsEvent::WhitelistUpdates, 1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn add_whitelist_updates(&self, hashes: Vec<(InfoHash, UpdatesAction)>) -> Vec<(InfoHash, bool)> { - let mut returned_data = Vec::new(); + let mut lock = self.torrents_whitelist_updates.write(); + let mut returned_data = Vec::with_capacity(hashes.len()); + let mut success_count = 0i64; + for (info_hash, updates_action) in hashes { - returned_data.push((info_hash, self.add_whitelist_update(info_hash, updates_action))); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + let success = lock.insert(timestamp, (info_hash, updates_action)).is_none(); + if success { + success_count += 1; + } + returned_data.push((info_hash, success)); + } + + if success_count > 0 { + self.update_stats(StatsEvent::WhitelistUpdates, success_count); } returned_data } @@ -38,30 +48,26 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_whitelist_updates(&self) -> HashMap { - let map = self.torrents_whitelist_updates.clone(); - let lock = map.read_recursive(); + let lock = self.torrents_whitelist_updates.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_whitelist_update(&self, timestamp: &u128) -> bool { - let map = self.torrents_whitelist_updates.clone(); - let mut lock = map.write(); - match lock.remove(timestamp) { - None => { false } - Some(_) => { - self.update_stats(StatsEvent::WhitelistUpdates, -1); - true - } + let mut lock = self.torrents_whitelist_updates.write(); + if lock.remove(timestamp).is_some() { + self.update_stats(StatsEvent::WhitelistUpdates, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_whitelist_updates(&self) { - let map = self.torrents_whitelist_updates.clone(); - let mut lock = map.write(); + let mut lock = self.torrents_whitelist_updates.write(); lock.clear(); self.set_stats(StatsEvent::WhitelistUpdates, 0); } @@ -69,30 +75,68 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_whitelist_updates(&self, torrent_tracker: Arc) -> Result<(), ()> { - let mut mapping: HashMap = HashMap::new(); - for (timestamp, (info_hash, updates_action)) in self.get_whitelist_updates().iter() { - match mapping.entry(*info_hash) { + let updates = { + let lock = self.torrents_whitelist_updates.read_recursive(); + lock.clone() + }; + + if updates.is_empty() { + return Ok(()); + } + + let mut mapping: HashMap = HashMap::with_capacity(updates.len()); + let mut timestamps_to_remove = Vec::new(); + + for (timestamp, (info_hash, updates_action)) in updates { + match mapping.entry(info_hash) { Entry::Occupied(mut o) => { - o.insert((o.get().0, *updates_action)); - self.remove_whitelist_update(timestamp); + let existing = o.get(); + if timestamp > existing.0 { + timestamps_to_remove.push(existing.0); + o.insert((timestamp, updates_action)); + } else { + timestamps_to_remove.push(timestamp); + } } Entry::Vacant(v) => { - v.insert((*timestamp, *updates_action)); + v.insert((timestamp, updates_action)); } } } - match self.save_whitelist(torrent_tracker.clone(), mapping.clone().into_iter().map(|(info_hash, (_, updates_action))| { - (info_hash, updates_action) - }).collect::>()).await { + + let mapping_len = mapping.len(); + let whitelist_updates: Vec<(InfoHash, UpdatesAction)> = mapping + .iter() + .map(|(info_hash, (_, updates_action))| (*info_hash, *updates_action)) + .collect(); + + match self.save_whitelist(torrent_tracker, whitelist_updates).await { Ok(_) => { - info!("[SYNC WHITELIST UPDATES] Synced {} whitelists", mapping.len()); - for (_, (timestamp, _)) in mapping.into_iter() { - self.remove_whitelist_update(×tamp); + info!("[SYNC WHITELIST UPDATES] Synced {mapping_len} whitelists"); + + let mut lock = self.torrents_whitelist_updates.write(); + let mut removed_count = 0i64; + + for (_, (timestamp, _)) in mapping { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } } + + for timestamp in timestamps_to_remove { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } + } + + if removed_count > 0 { + self.update_stats(StatsEvent::WhitelistUpdates, -removed_count); + } + Ok(()) } Err(_) => { - error!("[SYNC WHITELIST UPDATES] Unable to sync {} whitelists", mapping.len()); + error!("[SYNC WHITELIST UPDATES] Unable to sync {mapping_len} whitelists"); Err(()) } } diff --git a/src/tracker/impls/torrent_tracker_users.rs b/src/tracker/impls/torrent_tracker_users.rs index b27497c..a3d6bd7 100644 --- a/src/tracker/impls/torrent_tracker_users.rs +++ b/src/tracker/impls/torrent_tracker_users.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use chrono::{TimeZone, Utc}; use log::{error, info}; use crate::stats::enums::stats_event::StatsEvent; use crate::tracker::enums::updates_action::UpdatesAction; @@ -15,7 +14,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn load_users(&self, tracker: Arc) { - if let Ok(users) = self.sqlx.load_users(tracker.clone()).await { + if let Ok(users) = self.sqlx.load_users(tracker).await { info!("Loaded {users} users"); } } @@ -23,13 +22,14 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_users(&self, tracker: Arc, users: BTreeMap) -> Result<(), ()> { - match self.sqlx.save_users(tracker.clone(), users.clone()).await { + let users_len = users.len(); + match self.sqlx.save_users(tracker, users).await { Ok(_) => { - info!("[SYNC USERS] Synced {} users", users.len()); + info!("[SYNC USERS] Synced {users_len} users"); Ok(()) } Err(_) => { - error!("[SYNC USERS] Unable to sync {} users", users.len()); + error!("[SYNC USERS] Unable to sync {users_len} users"); Err(()) } } @@ -38,8 +38,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_user(&self, user_id: UserId, user_entry_item: UserEntryItem) -> bool { - let map = self.users.clone(); - let mut lock = map.write(); + let mut lock = self.users.write(); match lock.entry(user_id) { Entry::Vacant(v) => { self.update_stats(StatsEvent::Users, 1); @@ -56,16 +55,14 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_user_active_torrent(&self, user_id: UserId, info_hash: InfoHash) -> bool { - let map = self.users.clone(); - let mut lock = map.write(); + let mut lock = self.users.write(); match lock.entry(user_id) { Entry::Vacant(_) => { false } Entry::Occupied(mut o) => { - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - let timestamp_unix = timestamp.as_secs(); - o.get_mut().torrents_active.insert(info_hash, timestamp_unix); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + o.get_mut().torrents_active.insert(info_hash, timestamp); true } } @@ -74,47 +71,39 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn get_user(&self, id: UserId) -> Option { - let map = self.users.clone(); - let lock = map.read_recursive(); + let lock = self.users.read_recursive(); lock.get(&id).cloned() } #[tracing::instrument(level = "debug")] pub fn get_users(&self) -> BTreeMap { - let map = self.users.clone(); - let lock = map.read_recursive(); + let lock = self.users.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_user(&self, user_id: UserId) -> Option { - let map = self.users.clone(); - let mut lock = map.write(); - match lock.remove(&user_id) { - None => { None } - Some(data) => { - self.update_stats(StatsEvent::Users, -1); - Some(data) - } + let mut lock = self.users.write(); + if let Some(data) = lock.remove(&user_id) { + self.update_stats(StatsEvent::Users, -1); + Some(data) + } else { + None } } #[tracing::instrument(level = "debug")] pub fn remove_user_active_torrent(&self, user_id: UserId, info_hash: InfoHash) -> bool { - let map = self.users.clone(); - let mut lock = map.write(); + let mut lock = self.users.write(); match lock.entry(user_id) { Entry::Vacant(_) => { false } Entry::Occupied(mut o) => { - match o.get_mut().torrents_active.remove(&info_hash) { - None => { false } - Some(_) => { true } - } + o.get_mut().torrents_active.remove(&info_hash).is_some() } } } @@ -122,8 +111,7 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn check_user_key(&self, key: UserId) -> Option { - let map = self.users.clone(); - let lock = map.read_recursive(); + let lock = self.users.read_recursive(); for (user_id, user_entry_item) in lock.iter() { if user_entry_item.key == key { return Some(*user_id); @@ -135,24 +123,35 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn clean_user_active_torrents(&self, peer_timeout: Duration) { - let mut torrents_cleaned = 0u64; - let mut remove_active_torrents = vec![]; - let map = self.users.clone(); - let lock = map.read_recursive(); - info!("[USERS] Scanning {} users with dead active torrents", lock.len()); - for (user_id, user_entry_item) in lock.iter() { - let torrents_active = user_entry_item.torrents_active.clone(); - for (info_hash, updated) in torrents_active.iter() { - let time = SystemTime::from(Utc.timestamp_opt(*updated as i64 + peer_timeout.as_secs() as i64, 0).unwrap()); - if time.duration_since(SystemTime::now()).is_err() { - remove_active_torrents.push((*user_id, *info_hash)); + let current_time = SystemTime::now(); + let timeout_threshold = current_time.duration_since(UNIX_EPOCH).unwrap().as_secs() - peer_timeout.as_secs(); + + let remove_active_torrents = { + let lock = self.users.read_recursive(); + info!("[USERS] Scanning {} users with dead active torrents", lock.len()); + + let mut to_remove = Vec::new(); + for (user_id, user_entry_item) in lock.iter() { + for (info_hash, &updated) in &user_entry_item.torrents_active { + if updated < timeout_threshold { + to_remove.push((*user_id, *info_hash)); + } + } + } + to_remove + }; + + let torrents_cleaned = remove_active_torrents.len() as u64; + + if !remove_active_torrents.is_empty() { + let mut lock = self.users.write(); + for (user_id, info_hash) in remove_active_torrents { + if let Entry::Occupied(mut o) = lock.entry(user_id) { + o.get_mut().torrents_active.remove(&info_hash); } } } - for (user_id, info_hash) in remove_active_torrents { - self.remove_user_active_torrent(user_id, info_hash); - torrents_cleaned += 1; - } + info!("[USERS] Removed {torrents_cleaned} active torrents in users"); } } \ No newline at end of file diff --git a/src/tracker/impls/torrent_tracker_users_updates.rs b/src/tracker/impls/torrent_tracker_users_updates.rs index d6f4328..9041c1b 100644 --- a/src/tracker/impls/torrent_tracker_users_updates.rs +++ b/src/tracker/impls/torrent_tracker_users_updates.rs @@ -13,46 +13,40 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub fn add_user_update(&self, user_id: UserId, user_entry_item: UserEntryItem, updates_action: UpdatesAction) -> (UserEntryItem, bool) { - let map = self.users_updates.clone(); - let mut lock = map.write(); - match lock.insert(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(), (user_id, user_entry_item.clone(), updates_action)) { - None => { - self.update_stats(StatsEvent::UsersUpdates, 1); - (user_entry_item, true) - } - Some(_) => { - (user_entry_item, false) - } + let mut lock = self.users_updates.write(); + let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos(); + + if lock.insert(timestamp, (user_id, user_entry_item.clone(), updates_action)).is_none() { + self.update_stats(StatsEvent::UsersUpdates, 1); + (user_entry_item, true) + } else { + (user_entry_item, false) } } #[tracing::instrument(level = "debug")] pub fn get_user_updates(&self) -> HashMap { - let map = self.users_updates.clone(); - let lock = map.read_recursive(); + let lock = self.users_updates.read_recursive(); lock.clone() } #[tracing::instrument(level = "debug")] pub fn remove_user_update(&self, timestamp: &u128) -> bool { - let map = self.users_updates.clone(); - let mut lock = map.write(); - match lock.remove(timestamp) { - None => { false } - Some(_) => { - self.update_stats(StatsEvent::UsersUpdates, -1); - true - } + let mut lock = self.users_updates.write(); + if lock.remove(timestamp).is_some() { + self.update_stats(StatsEvent::UsersUpdates, -1); + true + } else { + false } } #[tracing::instrument(level = "debug")] pub fn clear_user_updates(&self) { - let map = self.users_updates.clone(); - let mut lock = map.write(); + let mut lock = self.users_updates.write(); lock.clear(); self.set_stats(StatsEvent::UsersUpdates, 0); } @@ -60,30 +54,68 @@ impl TorrentTracker { #[tracing::instrument(level = "debug")] pub async fn save_user_updates(&self, torrent_tracker: Arc) -> Result<(), ()> { - let mut mapping: HashMap = HashMap::new(); - for (timestamp, (user_id, user_entry_item, updates_action)) in self.get_user_updates().iter() { - match mapping.entry(*user_id) { + let updates = { + let lock = self.users_updates.read_recursive(); + lock.clone() + }; + + if updates.is_empty() { + return Ok(()); + } + + let mut mapping: HashMap = HashMap::with_capacity(updates.len()); + let mut timestamps_to_remove = Vec::new(); + + for (timestamp, (user_id, user_entry_item, updates_action)) in updates { + match mapping.entry(user_id) { Entry::Occupied(mut o) => { - o.insert((o.get().0, user_entry_item.clone(), *updates_action)); - self.remove_user_update(timestamp); + let existing = o.get(); + if timestamp > existing.0 { + timestamps_to_remove.push(existing.0); + o.insert((timestamp, user_entry_item, updates_action)); + } else { + timestamps_to_remove.push(timestamp); + } } Entry::Vacant(v) => { - v.insert((*timestamp, user_entry_item.clone(), *updates_action)); + v.insert((timestamp, user_entry_item, updates_action)); } } } - match self.save_users(torrent_tracker.clone(), mapping.clone().into_iter().map(|(user_id, (_, user_entry_item, updates_action))| { - (user_id, (user_entry_item.clone(), updates_action)) - }).collect::>()).await { + + let mapping_len = mapping.len(); + let users_to_save: BTreeMap = mapping + .iter() + .map(|(user_id, (_, user_entry_item, updates_action))| (*user_id, (user_entry_item.clone(), *updates_action))) + .collect(); + + match self.save_users(torrent_tracker, users_to_save).await { Ok(_) => { - info!("[SYNC USER UPDATES] Synced {} users", mapping.len()); - for (_, (timestamp, _, _)) in mapping.into_iter() { - self.remove_user_update(×tamp); + info!("[SYNC USER UPDATES] Synced {mapping_len} users"); + + let mut lock = self.users_updates.write(); + let mut removed_count = 0i64; + + for (_, (timestamp, _, _)) in mapping { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } + } + + for timestamp in timestamps_to_remove { + if lock.remove(×tamp).is_some() { + removed_count += 1; + } + } + + if removed_count > 0 { + self.update_stats(StatsEvent::UsersUpdates, -removed_count); } + Ok(()) } Err(_) => { - error!("[SYNC USER UPDATES] Unable to sync {} users", mapping.len()); + error!("[SYNC USER UPDATES] Unable to sync {mapping_len} users"); Err(()) } } diff --git a/src/tracker/impls/user_id.rs b/src/tracker/impls/user_id.rs index a019039..b18fe8f 100644 --- a/src/tracker/impls/user_id.rs +++ b/src/tracker/impls/user_id.rs @@ -2,7 +2,6 @@ use std::fmt; use std::fmt::Formatter; use crate::common::common::bin2hex; use crate::tracker::structs::user_id::UserId; -use crate::tracker::structs::user_id_visitor::UserIdVisitor; impl fmt::Display for UserId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -14,12 +13,25 @@ impl std::str::FromStr for UserId { type Err = binascii::ConvertError; fn from_str(s: &str) -> Result { - let mut i = Self([0u8; 20]); if s.len() != 40 { return Err(binascii::ConvertError::InvalidInputLength); } - binascii::hex2bin(s.as_bytes(), &mut i.0)?; - Ok(i) + + let mut result = UserId([0u8; 20]); + let bytes = s.as_bytes(); + + for i in 0..20 { + let high = hex_to_nibble(bytes[i * 2]); + let low = hex_to_nibble(bytes[i * 2 + 1]); + + if high == 0xFF || low == 0xFF { + return Err(binascii::ConvertError::InvalidInput); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) } } @@ -27,7 +39,7 @@ impl From<&[u8]> for UserId { fn from(data: &[u8]) -> UserId { assert_eq!(data.len(), 20); let mut ret = UserId([0u8; 20]); - ret.0.clone_from_slice(data); + ret.0.copy_from_slice(data); ret } } @@ -40,15 +52,73 @@ impl From<[u8; 20]> for UserId { impl serde::ser::Serialize for UserId { fn serialize(&self, serializer: S) -> Result { + const HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; let mut buffer = [0u8; 40]; - let bytes_out = binascii::bin2hex(&self.0, &mut buffer).ok().unwrap(); - let str_out = std::str::from_utf8(bytes_out).unwrap(); + + for (i, &byte) in self.0.iter().enumerate() { + buffer[i * 2] = HEX_CHARS[(byte >> 4) as usize]; + buffer[i * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize]; + } + + // SAFETY: We know the buffer contains only valid ASCII hex characters + let str_out = unsafe { std::str::from_utf8_unchecked(&buffer) }; serializer.serialize_str(str_out) } } impl<'de> serde::de::Deserialize<'de> for UserId { fn deserialize>(des: D) -> Result { + struct UserIdVisitor; + + impl<'de> serde::de::Visitor<'de> for UserIdVisitor { + type Value = UserId; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a 40 character hex string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v.len() != 40 { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"expected a 40 character long string", + )); + } + + let mut result = UserId([0u8; 20]); + let bytes = v.as_bytes(); + + for i in 0..20 { + let high = hex_to_nibble(bytes[i * 2]); + let low = hex_to_nibble(bytes[i * 2 + 1]); + + if high == 0xFF || low == 0xFF { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(v), + &"expected a hexadecimal string", + )); + } + + result.0[i] = (high << 4) | low; + } + + Ok(result) + } + } + des.deserialize_str(UserIdVisitor) } +} + +#[inline(always)] +fn hex_to_nibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => 0xFF, + } } \ No newline at end of file diff --git a/src/tracker/impls/user_id_visitor.rs b/src/tracker/impls/user_id_visitor.rs deleted file mode 100644 index 3c1b3d5..0000000 --- a/src/tracker/impls/user_id_visitor.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::fmt; -use std::fmt::Formatter; -use crate::tracker::structs::user_id::UserId; -use crate::tracker::structs::user_id_visitor::UserIdVisitor; - -impl serde::de::Visitor<'_> for UserIdVisitor { - type Value = UserId; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - write!(formatter, "a 40 character long hash") - } - - fn visit_str(self, v: &str) -> Result { - if v.len() != 40 { - return Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a 40 character long string", - )); - } - - let mut res = UserId([0u8; 20]); - - if binascii::hex2bin(v.as_bytes(), &mut res.0).is_err() { - Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(v), - &"expected a hexadecimal string", - )) - } else { - Ok(res) - } - } -} \ No newline at end of file diff --git a/src/tracker/structs.rs b/src/tracker/structs.rs index d477691..45f20e8 100644 --- a/src/tracker/structs.rs +++ b/src/tracker/structs.rs @@ -1,14 +1,13 @@ pub mod torrent_tracker; pub mod announce_query_request; pub mod info_hash; -pub mod info_hash_visitor; pub mod peer_id; -pub mod peer_id_visitor; pub mod scrape_query_request; pub mod torrent_entry; pub mod torrent_peer; pub mod user_entry_item; pub mod user_id; -pub mod user_id_visitor; pub mod torrent_peers; -pub mod torrent_sharding; \ No newline at end of file +pub mod torrent_sharding; +pub mod cleanup_stats_atomic; +pub mod padded_atomic_u64; \ No newline at end of file diff --git a/src/tracker/structs/cleanup_stats_atomic.rs b/src/tracker/structs/cleanup_stats_atomic.rs new file mode 100644 index 0000000..f04b030 --- /dev/null +++ b/src/tracker/structs/cleanup_stats_atomic.rs @@ -0,0 +1,7 @@ +use crate::tracker::structs::padded_atomic_u64::PaddedAtomicU64; + +pub struct CleanupStatsAtomic { + pub torrents: PaddedAtomicU64, + pub seeds: PaddedAtomicU64, + pub peers: PaddedAtomicU64, +} \ No newline at end of file diff --git a/src/tracker/structs/info_hash.rs b/src/tracker/structs/info_hash.rs index ece2c63..5089500 100644 --- a/src/tracker/structs/info_hash.rs +++ b/src/tracker/structs/info_hash.rs @@ -1,2 +1,2 @@ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug)] -pub struct InfoHash(pub [u8; 20]); +pub struct InfoHash(pub [u8; 20]); \ No newline at end of file diff --git a/src/tracker/structs/info_hash_visitor.rs b/src/tracker/structs/info_hash_visitor.rs deleted file mode 100644 index 6381032..0000000 --- a/src/tracker/structs/info_hash_visitor.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) struct InfoHashVisitor; \ No newline at end of file diff --git a/src/tracker/structs/padded_atomic_u64.rs b/src/tracker/structs/padded_atomic_u64.rs new file mode 100644 index 0000000..7b6943e --- /dev/null +++ b/src/tracker/structs/padded_atomic_u64.rs @@ -0,0 +1,8 @@ +use std::sync::atomic::AtomicU64; +use crate::tracker::impls::torrent_sharding::CACHE_LINE_SIZE; + +#[repr(align(64))] +pub struct PaddedAtomicU64 { + pub value: AtomicU64, + pub _padding: [u8; CACHE_LINE_SIZE - std::mem::size_of::()], +} \ No newline at end of file diff --git a/src/tracker/structs/peer_id_visitor.rs b/src/tracker/structs/peer_id_visitor.rs deleted file mode 100644 index 78ccd90..0000000 --- a/src/tracker/structs/peer_id_visitor.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) struct PeerIdVisitor; \ No newline at end of file diff --git a/src/tracker/structs/torrent_sharding.rs b/src/tracker/structs/torrent_sharding.rs index 5a2bd9d..161220f 100644 --- a/src/tracker/structs/torrent_sharding.rs +++ b/src/tracker/structs/torrent_sharding.rs @@ -4,262 +4,7 @@ use parking_lot::RwLock; use crate::tracker::structs::info_hash::InfoHash; use crate::tracker::structs::torrent_entry::TorrentEntry; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct TorrentSharding { - pub shard_000: Arc>>, - pub shard_001: Arc>>, - pub shard_002: Arc>>, - pub shard_003: Arc>>, - pub shard_004: Arc>>, - pub shard_005: Arc>>, - pub shard_006: Arc>>, - pub shard_007: Arc>>, - pub shard_008: Arc>>, - pub shard_009: Arc>>, - pub shard_010: Arc>>, - pub shard_011: Arc>>, - pub shard_012: Arc>>, - pub shard_013: Arc>>, - pub shard_014: Arc>>, - pub shard_015: Arc>>, - pub shard_016: Arc>>, - pub shard_017: Arc>>, - pub shard_018: Arc>>, - pub shard_019: Arc>>, - pub shard_020: Arc>>, - pub shard_021: Arc>>, - pub shard_022: Arc>>, - pub shard_023: Arc>>, - pub shard_024: Arc>>, - pub shard_025: Arc>>, - pub shard_026: Arc>>, - pub shard_027: Arc>>, - pub shard_028: Arc>>, - pub shard_029: Arc>>, - pub shard_030: Arc>>, - pub shard_031: Arc>>, - pub shard_032: Arc>>, - pub shard_033: Arc>>, - pub shard_034: Arc>>, - pub shard_035: Arc>>, - pub shard_036: Arc>>, - pub shard_037: Arc>>, - pub shard_038: Arc>>, - pub shard_039: Arc>>, - pub shard_040: Arc>>, - pub shard_041: Arc>>, - pub shard_042: Arc>>, - pub shard_043: Arc>>, - pub shard_044: Arc>>, - pub shard_045: Arc>>, - pub shard_046: Arc>>, - pub shard_047: Arc>>, - pub shard_048: Arc>>, - pub shard_049: Arc>>, - pub shard_050: Arc>>, - pub shard_051: Arc>>, - pub shard_052: Arc>>, - pub shard_053: Arc>>, - pub shard_054: Arc>>, - pub shard_055: Arc>>, - pub shard_056: Arc>>, - pub shard_057: Arc>>, - pub shard_058: Arc>>, - pub shard_059: Arc>>, - pub shard_060: Arc>>, - pub shard_061: Arc>>, - pub shard_062: Arc>>, - pub shard_063: Arc>>, - pub shard_064: Arc>>, - pub shard_065: Arc>>, - pub shard_066: Arc>>, - pub shard_067: Arc>>, - pub shard_068: Arc>>, - pub shard_069: Arc>>, - pub shard_070: Arc>>, - pub shard_071: Arc>>, - pub shard_072: Arc>>, - pub shard_073: Arc>>, - pub shard_074: Arc>>, - pub shard_075: Arc>>, - pub shard_076: Arc>>, - pub shard_077: Arc>>, - pub shard_078: Arc>>, - pub shard_079: Arc>>, - pub shard_080: Arc>>, - pub shard_081: Arc>>, - pub shard_082: Arc>>, - pub shard_083: Arc>>, - pub shard_084: Arc>>, - pub shard_085: Arc>>, - pub shard_086: Arc>>, - pub shard_087: Arc>>, - pub shard_088: Arc>>, - pub shard_089: Arc>>, - pub shard_090: Arc>>, - pub shard_091: Arc>>, - pub shard_092: Arc>>, - pub shard_093: Arc>>, - pub shard_094: Arc>>, - pub shard_095: Arc>>, - pub shard_096: Arc>>, - pub shard_097: Arc>>, - pub shard_098: Arc>>, - pub shard_099: Arc>>, - pub shard_100: Arc>>, - pub shard_101: Arc>>, - pub shard_102: Arc>>, - pub shard_103: Arc>>, - pub shard_104: Arc>>, - pub shard_105: Arc>>, - pub shard_106: Arc>>, - pub shard_107: Arc>>, - pub shard_108: Arc>>, - pub shard_109: Arc>>, - pub shard_110: Arc>>, - pub shard_111: Arc>>, - pub shard_112: Arc>>, - pub shard_113: Arc>>, - pub shard_114: Arc>>, - pub shard_115: Arc>>, - pub shard_116: Arc>>, - pub shard_117: Arc>>, - pub shard_118: Arc>>, - pub shard_119: Arc>>, - pub shard_120: Arc>>, - pub shard_121: Arc>>, - pub shard_122: Arc>>, - pub shard_123: Arc>>, - pub shard_124: Arc>>, - pub shard_125: Arc>>, - pub shard_126: Arc>>, - pub shard_127: Arc>>, - pub shard_128: Arc>>, - pub shard_129: Arc>>, - pub shard_130: Arc>>, - pub shard_131: Arc>>, - pub shard_132: Arc>>, - pub shard_133: Arc>>, - pub shard_134: Arc>>, - pub shard_135: Arc>>, - pub shard_136: Arc>>, - pub shard_137: Arc>>, - pub shard_138: Arc>>, - pub shard_139: Arc>>, - pub shard_140: Arc>>, - pub shard_141: Arc>>, - pub shard_142: Arc>>, - pub shard_143: Arc>>, - pub shard_144: Arc>>, - pub shard_145: Arc>>, - pub shard_146: Arc>>, - pub shard_147: Arc>>, - pub shard_148: Arc>>, - pub shard_149: Arc>>, - pub shard_150: Arc>>, - pub shard_151: Arc>>, - pub shard_152: Arc>>, - pub shard_153: Arc>>, - pub shard_154: Arc>>, - pub shard_155: Arc>>, - pub shard_156: Arc>>, - pub shard_157: Arc>>, - pub shard_158: Arc>>, - pub shard_159: Arc>>, - pub shard_160: Arc>>, - pub shard_161: Arc>>, - pub shard_162: Arc>>, - pub shard_163: Arc>>, - pub shard_164: Arc>>, - pub shard_165: Arc>>, - pub shard_166: Arc>>, - pub shard_167: Arc>>, - pub shard_168: Arc>>, - pub shard_169: Arc>>, - pub shard_170: Arc>>, - pub shard_171: Arc>>, - pub shard_172: Arc>>, - pub shard_173: Arc>>, - pub shard_174: Arc>>, - pub shard_175: Arc>>, - pub shard_176: Arc>>, - pub shard_177: Arc>>, - pub shard_178: Arc>>, - pub shard_179: Arc>>, - pub shard_180: Arc>>, - pub shard_181: Arc>>, - pub shard_182: Arc>>, - pub shard_183: Arc>>, - pub shard_184: Arc>>, - pub shard_185: Arc>>, - pub shard_186: Arc>>, - pub shard_187: Arc>>, - pub shard_188: Arc>>, - pub shard_189: Arc>>, - pub shard_190: Arc>>, - pub shard_191: Arc>>, - pub shard_192: Arc>>, - pub shard_193: Arc>>, - pub shard_194: Arc>>, - pub shard_195: Arc>>, - pub shard_196: Arc>>, - pub shard_197: Arc>>, - pub shard_198: Arc>>, - pub shard_199: Arc>>, - pub shard_200: Arc>>, - pub shard_201: Arc>>, - pub shard_202: Arc>>, - pub shard_203: Arc>>, - pub shard_204: Arc>>, - pub shard_205: Arc>>, - pub shard_206: Arc>>, - pub shard_207: Arc>>, - pub shard_208: Arc>>, - pub shard_209: Arc>>, - pub shard_210: Arc>>, - pub shard_211: Arc>>, - pub shard_212: Arc>>, - pub shard_213: Arc>>, - pub shard_214: Arc>>, - pub shard_215: Arc>>, - pub shard_216: Arc>>, - pub shard_217: Arc>>, - pub shard_218: Arc>>, - pub shard_219: Arc>>, - pub shard_220: Arc>>, - pub shard_221: Arc>>, - pub shard_222: Arc>>, - pub shard_223: Arc>>, - pub shard_224: Arc>>, - pub shard_225: Arc>>, - pub shard_226: Arc>>, - pub shard_227: Arc>>, - pub shard_228: Arc>>, - pub shard_229: Arc>>, - pub shard_230: Arc>>, - pub shard_231: Arc>>, - pub shard_232: Arc>>, - pub shard_233: Arc>>, - pub shard_234: Arc>>, - pub shard_235: Arc>>, - pub shard_236: Arc>>, - pub shard_237: Arc>>, - pub shard_238: Arc>>, - pub shard_239: Arc>>, - pub shard_240: Arc>>, - pub shard_241: Arc>>, - pub shard_242: Arc>>, - pub shard_243: Arc>>, - pub shard_244: Arc>>, - pub shard_245: Arc>>, - pub shard_246: Arc>>, - pub shard_247: Arc>>, - pub shard_248: Arc>>, - pub shard_249: Arc>>, - pub shard_250: Arc>>, - pub shard_251: Arc>>, - pub shard_252: Arc>>, - pub shard_253: Arc>>, - pub shard_254: Arc>>, - pub shard_255: Arc>>, -} \ No newline at end of file + pub shards: [Arc>>; 256], +} diff --git a/src/tracker/structs/torrent_tracker.rs b/src/tracker/structs/torrent_tracker.rs index f6f3d96..9d8a762 100644 --- a/src/tracker/structs/torrent_tracker.rs +++ b/src/tracker/structs/torrent_tracker.rs @@ -28,5 +28,5 @@ pub struct TorrentTracker { pub keys_updates: KeysUpdates, pub users: Arc>>, pub users_updates: UsersUpdates, - pub stats: Arc, + pub stats: Arc } \ No newline at end of file diff --git a/src/tracker/structs/user_id_visitor.rs b/src/tracker/structs/user_id_visitor.rs deleted file mode 100644 index 1f2863d..0000000 --- a/src/tracker/structs/user_id_visitor.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) struct UserIdVisitor; \ No newline at end of file diff --git a/src/udp/enums/request_parse_error.rs b/src/udp/enums/request_parse_error.rs index b7d0169..ae35e9a 100644 --- a/src/udp/enums/request_parse_error.rs +++ b/src/udp/enums/request_parse_error.rs @@ -1,5 +1,4 @@ -use std::io; -use actix_web::Either; +use std::borrow::Cow; use crate::udp::structs::connection_id::ConnectionId; use crate::udp::structs::transaction_id::TransactionId; @@ -8,9 +7,9 @@ pub enum RequestParseError { Sendable { connection_id: ConnectionId, transaction_id: TransactionId, - err: Either, + err: Cow<'static, str>, }, Unsendable { - err: Either, + err: Cow<'static, str>, }, } \ No newline at end of file diff --git a/src/udp/impls.rs b/src/udp/impls.rs index 6536388..62a5f9e 100644 --- a/src/udp/impls.rs +++ b/src/udp/impls.rs @@ -3,4 +3,5 @@ pub mod request; pub mod response; pub mod ipv4_addr; pub mod ipv6_addr; -pub mod udp_server; \ No newline at end of file +pub mod udp_server; +pub mod parse_pool; diff --git a/src/udp/impls/parse_pool.rs b/src/udp/impls/parse_pool.rs new file mode 100644 index 0000000..4c2c67c --- /dev/null +++ b/src/udp/impls/parse_pool.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; +use log::info; +use crossbeam::queue::ArrayQueue; +use crate::tracker::structs::torrent_tracker::TorrentTracker; +use crate::udp::structs::parse_pool::ParsePool; +use crate::udp::structs::udp_packet::UdpPacket; +use crate::udp::enums::request::Request; +use crate::udp::enums::response::Response; +use crate::udp::enums::server_error::ServerError; +use crate::udp::structs::transaction_id::TransactionId; +use crate::udp::structs::udp_server::UdpServer; +use crate::udp::udp::MAX_SCRAPE_TORRENTS; +use crate::stats::enums::stats_event::StatsEvent; + +impl Default for ParsePool { + fn default() -> Self { + Self::new(0) + } +} + +impl ParsePool { + pub fn new(capacity: usize) -> ParsePool { + ParsePool { payload: Arc::new(ArrayQueue::new(capacity)) } + } + + pub async fn start_thread(&self, threads: usize, tracker: Arc, shutdown_handler: tokio::sync::watch::Receiver) { + for i in 0..threads { + let payload = self.payload.clone(); + let tracker_cloned = tracker.clone(); + let mut shutdown_handler = shutdown_handler.clone(); + + tokio::spawn(async move { + info!("[UDP] Start Parse Pool thread {i}..."); + let mut batch: Vec = Vec::with_capacity(64); + + const BATCH_MAX: usize = 64; + const EMPTY_YIELD_EVERY: usize = 256; + let mut empty_polls = 0usize; + + loop { + batch.clear(); + while let Some(packet) = payload.pop() { + batch.push(packet); + if batch.len() >= BATCH_MAX { + break; + } + } + + if !batch.is_empty() { + Self::process_batch(&batch, tracker_cloned.clone()).await; + empty_polls = 0; + } else { + empty_polls += 1; + if empty_polls % EMPTY_YIELD_EVERY == 0 { + tokio::task::yield_now().await; + } + } + + if shutdown_handler.has_changed().unwrap_or(false) + && shutdown_handler.changed().await.is_ok() { + info!("[UDP] Shutting down the Parse Pool thread {i}..."); + return; + } + } + }); + } + } + + async fn process_batch(packets: &[UdpPacket], tracker: Arc) { + for packet in packets { + let payload = &packet.data[..packet.data_len]; + + let response = match Request::from_bytes(payload, MAX_SCRAPE_TORRENTS) { + Ok(request) => { + match UdpServer::handle_request(request, packet.remote_addr, tracker.clone()).await { + Ok(resp) => resp, + Err(_e) => { + match packet.remote_addr { + std::net::SocketAddr::V4(_) => { tracker.update_stats(StatsEvent::Udp4InvalidRequest, 1); } + std::net::SocketAddr::V6(_) => { tracker.update_stats(StatsEvent::Udp6InvalidRequest, 1); } + } + Response::from(crate::udp::structs::error_response::ErrorResponse { + transaction_id: TransactionId(0), + message: ServerError::BadRequest.to_string().into(), + }) + } + } + } + Err(_) => { + match packet.remote_addr { + std::net::SocketAddr::V4(_) => { tracker.update_stats(StatsEvent::Udp4BadRequest, 1); } + std::net::SocketAddr::V6(_) => { tracker.update_stats(StatsEvent::Udp6BadRequest, 1); } + } + Response::from(crate::udp::structs::error_response::ErrorResponse { + transaction_id: TransactionId(0), + message: ServerError::BadRequest.to_string().into(), + }) + } + }; + + UdpServer::send_response(tracker.clone(), packet.socket.clone(), packet.remote_addr, response).await; + } + } +} \ No newline at end of file diff --git a/src/udp/impls/request.rs b/src/udp/impls/request.rs index 357127a..b0e15b2 100644 --- a/src/udp/impls/request.rs +++ b/src/udp/impls/request.rs @@ -83,26 +83,42 @@ impl Request { #[tracing::instrument(level = "debug")] pub fn from_bytes(bytes: &[u8], max_scrape_torrents: u8) -> Result { - let mut cursor = Cursor::new(bytes); + if bytes.len() < 16 { + return Err(RequestParseError::unsendable_text("Packet too short")); + } + + let connection_id = i64::from_be_bytes(bytes[0..8].try_into().map_err(|_| + RequestParseError::unsendable_io(io::Error::new(io::ErrorKind::InvalidData, "Invalid connection_id")) + )?); - let connection_id = cursor - .read_i64::() - .map_err(RequestParseError::unsendable_io)?; - let action = cursor - .read_i32::() - .map_err(RequestParseError::unsendable_io)?; - let transaction_id = cursor - .read_i32::() - .map_err(RequestParseError::unsendable_io)?; + let action = i32::from_be_bytes(bytes[8..12].try_into().map_err(|_| + RequestParseError::unsendable_io(io::Error::new(io::ErrorKind::InvalidData, "Invalid action")) + )?); + + let transaction_id = i32::from_be_bytes(bytes[12..16].try_into().map_err(|_| + RequestParseError::unsendable_io(io::Error::new(io::ErrorKind::InvalidData, "Invalid transaction_id")) + )?); + + if action == 0 { + if connection_id == PROTOCOL_IDENTIFIER { + return Ok(ConnectRequest { + transaction_id: TransactionId(transaction_id), + }.into()); + } else { + return Err(RequestParseError::unsendable_text("Protocol identifier missing")); + } + } + + let mut cursor = Cursor::new(bytes); + cursor.set_position(16); match action { // Connect 0 => { if connection_id == PROTOCOL_IDENTIFIER { - Ok((ConnectRequest { + Ok(ConnectRequest { transaction_id: TransactionId(transaction_id), - }) - .into()) + }.into()) } else { Err(RequestParseError::unsendable_text( "Protocol identifier missing", @@ -116,39 +132,23 @@ impl Request { let mut peer_id = [0; 20]; let mut ip = [0; 4]; - cursor.read_exact(&mut info_hash).map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - cursor.read_exact(&mut peer_id).map_err(|err| { + let sendable_err = |err: io::Error| { RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; + }; - let bytes_downloaded = cursor.read_i64::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - let bytes_left = cursor.read_i64::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - let bytes_uploaded = cursor.read_i64::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - let event = cursor.read_i32::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; + cursor.read_exact(&mut info_hash).map_err(sendable_err)?; + cursor.read_exact(&mut peer_id).map_err(sendable_err)?; - cursor.read_exact(&mut ip).map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; + let bytes_downloaded = cursor.read_i64::().map_err(sendable_err)?; + let bytes_left = cursor.read_i64::().map_err(sendable_err)?; + let bytes_uploaded = cursor.read_i64::().map_err(sendable_err)?; + let event = cursor.read_i32::().map_err(sendable_err)?; - let key = cursor.read_u32::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - let peers_wanted = cursor.read_i32::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - let port = cursor.read_u16::().map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; + cursor.read_exact(&mut ip).map_err(sendable_err)?; + + let key = cursor.read_u32::().map_err(sendable_err)?; + let peers_wanted = cursor.read_i32::().map_err(sendable_err)?; + let port = cursor.read_u16::().map_err(sendable_err)?; let opt_ip = if ip == [0; 4] { None @@ -156,22 +156,33 @@ impl Request { Some(Ipv4Addr::from(ip)) }; - let option_byte = cursor.read_u8(); - let option_size = cursor.read_u8(); - let mut path: &str = ""; - let mut path_array = vec![]; - - let option_byte_value = option_byte.unwrap_or_default(); - let option_size_value = option_size.unwrap_or_default(); - if option_byte_value == 2 { - path_array.resize(option_size_value as usize, 0u8); - cursor.read_exact(&mut path_array).map_err(|err| { - RequestParseError::sendable_io(err, connection_id, transaction_id) - })?; - path = std::str::from_utf8(&path_array).unwrap_or_default(); - } + let path = if cursor.position() < bytes.len() as u64 { + let option_byte = cursor.read_u8().ok(); + let option_size = cursor.read_u8().ok(); + + if option_byte == Some(2) { + if let Some(size) = option_size { + let size_usize = size as usize; + if cursor.position() + size_usize as u64 <= bytes.len() as u64 { + let start_pos = cursor.position() as usize; + let end_pos = start_pos + size_usize; + std::str::from_utf8(&bytes[start_pos..end_pos]) + .unwrap_or_default() + .to_string() + } else { + String::new() + } + } else { + String::new() + } + } else { + String::new() + } + } else { + String::new() + }; - Ok((AnnounceRequest { + Ok(AnnounceRequest { connection_id: ConnectionId(connection_id), transaction_id: TransactionId(transaction_id), info_hash: InfoHash(info_hash), @@ -184,36 +195,44 @@ impl Request { key: PeerKey(key), peers_wanted: NumberOfPeers(peers_wanted), port: Port(port), - path: path.to_string(), - }) - .into()) + path, + }.into()) } // Scrape 2 => { let position = cursor.position() as usize; - let inner = cursor.into_inner(); + let remaining_bytes = &bytes[position..]; - let info_hashes: Vec = inner[position..] - .chunks_exact(20) - .take(max_scrape_torrents as usize) - .map(|chunk| InfoHash(chunk.try_into().unwrap())) - .collect(); + let max_hashes = max_scrape_torrents as usize; + let available_hashes = remaining_bytes.len() / 20; + let actual_hashes = available_hashes.min(max_hashes); - if info_hashes.is_empty() { - Err(RequestParseError::sendable_text( + if actual_hashes == 0 { + return Err(RequestParseError::sendable_text( "Full scrapes are not allowed", connection_id, transaction_id, - )) - } else { - Ok((ScrapeRequest { - connection_id: ConnectionId(connection_id), - transaction_id: TransactionId(transaction_id), - info_hashes, - }) - .into()) + )); + } + + let mut info_hashes = Vec::with_capacity(actual_hashes); + + for chunk in remaining_bytes.chunks_exact(20).take(actual_hashes) { + let hash_array: [u8; 20] = chunk.try_into() + .map_err(|_| RequestParseError::sendable_text( + "Invalid info hash format", + connection_id, + transaction_id, + ))?; + info_hashes.push(InfoHash(hash_array)); } + + Ok(ScrapeRequest { + connection_id: ConnectionId(connection_id), + transaction_id: TransactionId(transaction_id), + info_hashes, + }.into()) } _ => Err(RequestParseError::sendable_text( diff --git a/src/udp/impls/request_parse_error.rs b/src/udp/impls/request_parse_error.rs index f04e7d6..40aca5b 100644 --- a/src/udp/impls/request_parse_error.rs +++ b/src/udp/impls/request_parse_error.rs @@ -1,5 +1,5 @@ use std::io; -use actix_web::Either; +use std::borrow::Cow; use crate::udp::enums::request_parse_error::RequestParseError; use crate::udp::structs::connection_id::ConnectionId; use crate::udp::structs::transaction_id::TransactionId; @@ -10,27 +10,30 @@ impl RequestParseError { Self::Sendable { connection_id: ConnectionId(connection_id), transaction_id: TransactionId(transaction_id), - err: Either::Left(err), + err: Cow::Owned(err.to_string()), } } + #[tracing::instrument(level = "debug")] pub fn sendable_text(text: &'static str, connection_id: i64, transaction_id: i32) -> Self { Self::Sendable { connection_id: ConnectionId(connection_id), transaction_id: TransactionId(transaction_id), - err: Either::Right(text), + err: Cow::Borrowed(text), } } + #[tracing::instrument(level = "debug")] pub fn unsendable_io(err: io::Error) -> Self { Self::Unsendable { - err: Either::Left(err), + err: Cow::Owned(err.to_string()), } } + #[tracing::instrument(level = "debug")] pub fn unsendable_text(text: &'static str) -> Self { Self::Unsendable { - err: Either::Right(text), + err: Cow::Borrowed(text), } } } \ No newline at end of file diff --git a/src/udp/impls/response.rs b/src/udp/impls/response.rs index fcc37a8..cc27482 100644 --- a/src/udp/impls/response.rs +++ b/src/udp/impls/response.rs @@ -64,9 +64,14 @@ impl Response { bytes.write_i32::(r.leechers.0)?; bytes.write_i32::(r.seeders.0)?; - for peer in r.peers.iter() { - bytes.write_all(&peer.ip_address.octets())?; - bytes.write_u16::(peer.port.0)?; + let peer_count = r.peers.len(); + if peer_count > 0 { + let mut peer_buffer = Vec::with_capacity(peer_count * 6); + for peer in &r.peers { + peer_buffer.extend_from_slice(&peer.ip_address.octets()); + peer_buffer.write_u16::(peer.port.0)?; + } + bytes.write_all(&peer_buffer)?; } } Response::AnnounceIpv6(r) => { @@ -76,25 +81,34 @@ impl Response { bytes.write_i32::(r.leechers.0)?; bytes.write_i32::(r.seeders.0)?; - for peer in r.peers.iter() { - bytes.write_all(&peer.ip_address.octets())?; - bytes.write_u16::(peer.port.0)?; + let peer_count = r.peers.len(); + if peer_count > 0 { + let mut peer_buffer = Vec::with_capacity(peer_count * 18); + for peer in &r.peers { + peer_buffer.extend_from_slice(&peer.ip_address.octets()); + peer_buffer.write_u16::(peer.port.0)?; + } + bytes.write_all(&peer_buffer)?; } } Response::Scrape(r) => { bytes.write_i32::(2)?; bytes.write_i32::(r.transaction_id.0)?; - for torrent_stat in r.torrent_stats.iter() { - bytes.write_i32::(torrent_stat.seeders.0)?; - bytes.write_i32::(torrent_stat.completed.0)?; - bytes.write_i32::(torrent_stat.leechers.0)?; + let stats_count = r.torrent_stats.len(); + if stats_count > 0 { + let mut stats_buffer = Vec::with_capacity(stats_count * 12); + for torrent_stat in &r.torrent_stats { + stats_buffer.write_i32::(torrent_stat.seeders.0)?; + stats_buffer.write_i32::(torrent_stat.completed.0)?; + stats_buffer.write_i32::(torrent_stat.leechers.0)?; + } + bytes.write_all(&stats_buffer)?; } } Response::Error(r) => { bytes.write_i32::(3)?; bytes.write_i32::(r.transaction_id.0)?; - bytes.write_all(r.message.as_bytes())?; } } @@ -115,121 +129,168 @@ impl Response { 0 => { let connection_id = cursor.read_i64::()?; - Ok((ConnectResponse { + Ok(ConnectResponse { connection_id: ConnectionId(connection_id), transaction_id: TransactionId(transaction_id), - }) + } .into()) } // Announce - 1 if ipv4 => { + 1 => { let announce_interval = cursor.read_i32::()?; let leechers = cursor.read_i32::()?; let seeders = cursor.read_i32::()?; let position = cursor.position() as usize; - let inner = cursor.into_inner(); - - let peers = inner[position..] - .chunks_exact(6) - .map(|chunk| { - let ip_bytes: [u8; 4] = (&chunk[..4]).try_into().unwrap(); - let ip_address = Ipv4Addr::from(ip_bytes); - let port = (&chunk[4..]).read_u16::().unwrap(); - - ResponsePeer { - ip_address, - port: Port(port), - } - }) - .collect(); - - Ok((AnnounceResponse { - transaction_id: TransactionId(transaction_id), - announce_interval: AnnounceInterval(announce_interval), - leechers: NumberOfPeers(leechers), - seeders: NumberOfPeers(seeders), - peers, - }) - .into()) - } - 1 if !ipv4 => { - let announce_interval = cursor.read_i32::()?; - let leechers = cursor.read_i32::()?; - let seeders = cursor.read_i32::()?; + let remaining_bytes = &bytes[position..]; - let position = cursor.position() as usize; - let inner = cursor.into_inner(); - - let peers = inner[position..] - .chunks_exact(18) - .map(|chunk| { - let ip_bytes: [u8; 16] = (&chunk[..16]).try_into().unwrap(); - let ip_address = Ipv6Addr::from(ip_bytes); - let port = (&chunk[16..]).read_u16::().unwrap(); - - ResponsePeer { - ip_address, - port: Port(port), - } - }) - .collect(); - - Ok((AnnounceResponse { - transaction_id: TransactionId(transaction_id), - announce_interval: AnnounceInterval(announce_interval), - leechers: NumberOfPeers(leechers), - seeders: NumberOfPeers(seeders), - peers, - }) - .into()) + if ipv4 { + let peers = parse_ipv4_peers(remaining_bytes)?; + Ok(AnnounceResponse { + transaction_id: TransactionId(transaction_id), + announce_interval: AnnounceInterval(announce_interval), + leechers: NumberOfPeers(leechers), + seeders: NumberOfPeers(seeders), + peers, + } + .into()) + } else { + let peers = parse_ipv6_peers(remaining_bytes)?; + Ok(AnnounceResponse { + transaction_id: TransactionId(transaction_id), + announce_interval: AnnounceInterval(announce_interval), + leechers: NumberOfPeers(leechers), + seeders: NumberOfPeers(seeders), + peers, + } + .into()) + } } // Scrape 2 => { let position = cursor.position() as usize; - let inner = cursor.into_inner(); - - let stats = inner[position..] - .chunks_exact(12) - .map(|chunk| { - let mut cursor: Cursor<&[u8]> = Cursor::new(chunk); - - let seeders = cursor.read_i32::().unwrap(); - let downloads = cursor.read_i32::().unwrap(); - let leechers = cursor.read_i32::().unwrap(); - - TorrentScrapeStatistics { - seeders: NumberOfPeers(seeders), - completed: NumberOfDownloads(downloads), - leechers: NumberOfPeers(leechers), - } - }) - .collect(); - - Ok((ScrapeResponse { + let remaining_bytes = &bytes[position..]; + + let torrent_stats = parse_scrape_stats(remaining_bytes)?; + Ok(ScrapeResponse { transaction_id: TransactionId(transaction_id), - torrent_stats: stats, - }) + torrent_stats, + } .into()) } // Error 3 => { let position = cursor.position() as usize; - let inner = cursor.into_inner(); + let message_bytes = &bytes[position..]; + let message = String::from_utf8_lossy(message_bytes).into_owned(); - Ok((ErrorResponse { + Ok(ErrorResponse { transaction_id: TransactionId(transaction_id), - message: String::from_utf8_lossy(&inner[position..]) - .into_owned() - .into(), - }) + message: message.into(), + } .into()) } - _ => Ok((ErrorResponse { + _ => Ok(ErrorResponse { transaction_id: TransactionId(transaction_id), message: "Invalid action".into(), - }) + } .into()), } } + + #[inline] + pub fn estimated_size(&self) -> usize { + match self { + Response::Connect(_) => 16, + Response::AnnounceIpv4(r) => 20 + (r.peers.len() * 6), + Response::AnnounceIpv6(r) => 20 + (r.peers.len() * 18), + Response::Scrape(r) => 8 + (r.torrent_stats.len() * 12), + Response::Error(r) => 8 + r.message.len(), + } + } + + #[inline] + pub fn write_to_vec(&self) -> Result, io::Error> { + let estimated_size = self.estimated_size(); + let mut buffer = Vec::with_capacity(estimated_size); + self.write(&mut buffer)?; + Ok(buffer) + } +} + +#[inline] +fn parse_ipv4_peers(bytes: &[u8]) -> Result>, io::Error> { + let chunk_size = 6; + let peer_count = bytes.len() / chunk_size; + let mut peers = Vec::with_capacity(peer_count); + + for chunk in bytes.chunks_exact(chunk_size) { + let ip_bytes: [u8; 4] = chunk[..4].try_into().map_err(|_| + io::Error::new(io::ErrorKind::InvalidData, "Invalid IPv4 address bytes") + )?; + + let port = (&chunk[4..6]).read_u16::().map_err(|e| + io::Error::new(io::ErrorKind::InvalidData, e) + )?; + + peers.push(ResponsePeer { + ip_address: Ipv4Addr::from(ip_bytes), + port: Port(port), + }); + } + + Ok(peers) +} + +#[inline] +fn parse_ipv6_peers(bytes: &[u8]) -> Result>, io::Error> { + let chunk_size = 18; + let peer_count = bytes.len() / chunk_size; + let mut peers = Vec::with_capacity(peer_count); + + for chunk in bytes.chunks_exact(chunk_size) { + let ip_bytes: [u8; 16] = chunk[..16].try_into().map_err(|_| + io::Error::new(io::ErrorKind::InvalidData, "Invalid IPv6 address bytes") + )?; + + let port = (&chunk[16..18]).read_u16::().map_err(|e| + io::Error::new(io::ErrorKind::InvalidData, e) + )?; + + peers.push(ResponsePeer { + ip_address: Ipv6Addr::from(ip_bytes), + port: Port(port), + }); + } + + Ok(peers) +} + +#[inline] +fn parse_scrape_stats(bytes: &[u8]) -> Result, io::Error> { + let chunk_size = 12; + let stats_count = bytes.len() / chunk_size; + let mut stats = Vec::with_capacity(stats_count); + + for chunk in bytes.chunks_exact(chunk_size) { + let mut cursor = Cursor::new(chunk); + + let seeders = cursor.read_i32::().map_err(|e| + io::Error::new(io::ErrorKind::InvalidData, e) + )?; + let downloads = cursor.read_i32::().map_err(|e| + io::Error::new(io::ErrorKind::InvalidData, e) + )?; + let leechers = cursor.read_i32::().map_err(|e| + io::Error::new(io::ErrorKind::InvalidData, e) + )?; + + stats.push(TorrentScrapeStatistics { + seeders: NumberOfPeers(seeders), + completed: NumberOfDownloads(downloads), + leechers: NumberOfPeers(leechers), + }); + } + + Ok(stats) } \ No newline at end of file diff --git a/src/udp/impls/udp_server.rs b/src/udp/impls/udp_server.rs index 55447db..1237880 100644 --- a/src/udp/impls/udp_server.rs +++ b/src/udp/impls/udp_server.rs @@ -1,10 +1,10 @@ -use std::io::Cursor; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; -use std::time::SystemTime; +use std::time::Duration; use log::{debug, info}; use socket2::{Socket, Domain, Type, Protocol}; use tokio::net::UdpSocket; +use tokio::runtime::Builder; use crate::stats::enums::stats_event::StatsEvent; use crate::tracker::enums::torrent_peers_type::TorrentPeersType; use crate::tracker::structs::announce_query_request::AnnounceQueryRequest; @@ -24,106 +24,133 @@ use crate::udp::structs::connection_id::ConnectionId; use crate::udp::structs::error_response::ErrorResponse; use crate::udp::structs::number_of_downloads::NumberOfDownloads; use crate::udp::structs::number_of_peers::NumberOfPeers; +use crate::udp::structs::parse_pool::ParsePool; use crate::udp::structs::port::Port; use crate::udp::structs::response_peer::ResponsePeer; use crate::udp::structs::scrape_request::ScrapeRequest; use crate::udp::structs::scrape_response::ScrapeResponse; use crate::udp::structs::torrent_scrape_statistics::TorrentScrapeStatistics; use crate::udp::structs::transaction_id::TransactionId; +use crate::udp::structs::udp_packet::UdpPacket; use crate::udp::structs::udp_server::UdpServer; -use crate::udp::udp::{MAX_PACKET_SIZE, MAX_SCRAPE_TORRENTS}; +use crate::udp::udp::MAX_SCRAPE_TORRENTS; impl UdpServer { #[tracing::instrument(level = "debug")] - pub async fn new(tracker: Arc, bind_address: SocketAddr, threads: u64, recv_buffer_size: usize, send_buffer_size: usize, reuse_address: bool) -> tokio::io::Result + pub async fn new(tracker: Arc, bind_address: SocketAddr, udp_threads: usize, worker_threads: usize, recv_buffer_size: usize, send_buffer_size: usize, reuse_address: bool) -> tokio::io::Result { let domain = if bind_address.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 }; let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; - socket.set_recv_buffer_size(recv_buffer_size).map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; - socket.set_send_buffer_size(send_buffer_size).map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; - socket.set_reuse_address(reuse_address).map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; - socket.bind(&bind_address.into()).map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; - socket.set_nonblocking(true).map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; + socket.set_recv_buffer_size(recv_buffer_size).map_err(tokio::io::Error::other)?; + socket.set_send_buffer_size(send_buffer_size).map_err(tokio::io::Error::other)?; + socket.set_reuse_address(reuse_address).map_err(tokio::io::Error::other)?; + socket.bind(&bind_address.into()).map_err(tokio::io::Error::other)?; + socket.set_nonblocking(true).map_err(tokio::io::Error::other)?; - // Convert to std::net::UdpSocket, then to tokio::net::UdpSocket let std_socket: std::net::UdpSocket = socket.into(); let tokio_socket = UdpSocket::from_std(std_socket)?; Ok(UdpServer { socket: Arc::new(tokio_socket), - threads, + udp_threads, + worker_threads, tracker, }) } #[tracing::instrument(level = "debug")] - pub async fn start(&self, rx: tokio::sync::watch::Receiver) - { - let threads = self.threads; - for _index in 0..=threads { - let socket_clone = self.socket.clone(); - let tracker = self.tracker.clone(); - let mut rx = rx.clone(); - let mut data = [0; 65507]; - tokio::spawn(async move { - loop { - let udp_sock = socket_clone.local_addr().unwrap(); - tokio::select! { - _ = rx.changed() => { - info!("Stopping UDP server: {udp_sock}..."); - break; - } - Ok((valid_bytes, remote_addr)) = socket_clone.recv_from(&mut data) => { - let payload = data[..valid_bytes].to_vec(); - - debug!("Received {} bytes from {}", payload.len(), remote_addr); - debug!("{payload:?}"); - - let remote_addr_cloned = remote_addr; - let payload_cloned = payload.clone(); - let tracker_cloned = tracker.clone(); - let socket_cloned = socket_clone.clone(); - tokio::spawn(async move { - let response = UdpServer::handle_packet(remote_addr_cloned, payload_cloned, tracker_cloned.clone()).await; - UdpServer::send_response(tracker_cloned.clone(), socket_cloned.clone(), remote_addr_cloned, response).await; - }); - } + pub async fn start(&self, mut rx: tokio::sync::watch::Receiver) { + let parse_pool = Arc::new(ParsePool::new(10000)); + parse_pool.start_thread(self.worker_threads, self.tracker.clone(), rx.clone()).await; + + let payload = parse_pool.payload.clone(); + let tracker_queue = self.tracker.clone(); + let mut rx_queue = rx.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(1)); + loop { + tokio::select! { + _ = rx_queue.changed() => { + break; } + _ = interval.tick() => { + let len = payload.len() as i64; + tracker_queue.set_stats(StatsEvent::UdpQueueLen, len); + } + } + } + }); + + let udp_threads = self.udp_threads; + let socket_clone = self.socket.clone(); + let parse_pool_clone = parse_pool.clone(); + + tokio::task::spawn_blocking(move || { + let tokio_udp = Builder::new_multi_thread() + .thread_name("udp") + .worker_threads(udp_threads) + .enable_all() + .build() + .unwrap(); + + tokio_udp.block_on(async move { + for _index in 0..udp_threads { + let parse_pool_clone = parse_pool_clone.clone(); + let socket_clone = socket_clone.clone(); + let mut rx = rx.clone(); + + tokio::spawn(async move { + let mut data = [0; 1496]; + loop { + let udp_sock = socket_clone.local_addr().unwrap(); + tokio::select! { + _ = rx.changed() => { + info!("Stopping UDP server: {udp_sock}..."); + break; + } + Ok((valid_bytes, remote_addr)) = socket_clone.recv_from(&mut data) => { + if valid_bytes > 0 { + let packet = UdpPacket { + remote_addr, + data, + data_len: valid_bytes, + socket: socket_clone.clone(), + }; + + if parse_pool_clone.payload.push(packet).is_err() { + debug!("Parse pool queue full, dropping packet"); + } + } + } + } + } + }); } + rx.changed().await.ok(); }); - } + }); } #[tracing::instrument(level = "debug")] - pub async fn send_response(tracker: Arc, socket: Arc, remote_addr: SocketAddr, response: Response) - { + pub async fn send_response(tracker: Arc, socket: Arc, remote_addr: SocketAddr, response: Response) { debug!("sending response to: {:?}", &remote_addr); - let sentry = sentry::TransactionContext::new("udp server", "send response"); - let transaction = sentry::start_transaction(sentry); - let buffer = vec![0u8; MAX_PACKET_SIZE]; - let mut cursor = Cursor::new(buffer); + let estimated_size = response.estimated_size(); + let mut buffer = Vec::with_capacity(estimated_size); - match response.write(&mut cursor) { + match response.write(&mut buffer) { Ok(_) => { - let position = cursor.position() as usize; - let inner = cursor.get_ref(); - - debug!("{:?}", &inner[..position]); - UdpServer::send_packet(socket, &remote_addr, &inner[..position]).await; + UdpServer::send_packet(socket, &remote_addr, &buffer).await; } Err(error) => { - sentry::capture_error(&error); match remote_addr { SocketAddr::V4(_) => { tracker.update_stats(StatsEvent::Udp4InvalidRequest, 1); } SocketAddr::V6(_) => { tracker.update_stats(StatsEvent::Udp6InvalidRequest, 1); } } - debug!("could not write response to bytes."); + debug!("could not write response to bytes: {error}"); } } - - transaction.finish(); } #[tracing::instrument(level = "debug")] @@ -133,36 +160,49 @@ impl UdpServer { #[tracing::instrument(level = "debug")] pub async fn get_connection_id(remote_address: &SocketAddr) -> ConnectionId { - match SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => ConnectionId(((duration.as_secs() / 3600) | ((remote_address.port() as u64) << 36)) as i64), - Err(_) => ConnectionId(0x7FFFFFFFFFFFFFFF) + use std::hash::{Hasher, DefaultHasher}; + use std::time::Instant; + + let mut hasher = DefaultHasher::new(); + hasher.write_u64(Instant::now().elapsed().as_nanos() as u64); + hasher.write_u16(remote_address.port()); + if let std::net::IpAddr::V4(ipv4) = remote_address.ip() { + hasher.write(&ipv4.octets()); + } else if let std::net::IpAddr::V6(ipv6) = remote_address.ip() { + hasher.write(&ipv6.octets()); } + + ConnectionId(hasher.finish() as i64) } #[tracing::instrument(level = "debug")] - pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: Arc) -> Response { - match Request::from_bytes(&payload[..payload.len()], MAX_SCRAPE_TORRENTS).map_err(|_| ServerError::InternalServerError) { + pub async fn handle_packet(remote_addr: SocketAddr, payload: &[u8], tracker: Arc) -> Response { + if payload.len() >= 16 + && payload[8..12] == [0, 0, 0, 0] { + if let Ok(Request::Connect(connect_request)) = Request::from_bytes(payload, MAX_SCRAPE_TORRENTS) { + return match UdpServer::handle_udp_connect(remote_addr, &connect_request, tracker).await { + Ok(response) => response, + Err(e) => UdpServer::handle_udp_error(e, connect_request.transaction_id).await, + }; + } + } + + let transaction_id = match Request::from_bytes(payload, MAX_SCRAPE_TORRENTS) { Ok(request) => { - let transaction_id = match &request { - Request::Connect(connect_request) => { - connect_request.transaction_id - } - Request::Announce(announce_request) => { - announce_request.transaction_id - } - Request::Scrape(scrape_request) => { - scrape_request.transaction_id - } + let tid = match &request { + Request::Connect(connect_request) => connect_request.transaction_id, + Request::Announce(announce_request) => announce_request.transaction_id, + Request::Scrape(scrape_request) => scrape_request.transaction_id, }; match UdpServer::handle_request(request, remote_addr, tracker.clone()).await { - Ok(response) => response, - Err(e) => { + Ok(response) => return response, + Err(_e) => { match remote_addr { SocketAddr::V4(_) => { tracker.update_stats(StatsEvent::Udp4InvalidRequest, 1); } SocketAddr::V6(_) => { tracker.update_stats(StatsEvent::Udp6InvalidRequest, 1); } } - UdpServer::handle_udp_error(e, transaction_id).await + tid } } } @@ -171,9 +211,11 @@ impl UdpServer { SocketAddr::V4(_) => { tracker.update_stats(StatsEvent::Udp4BadRequest, 1); } SocketAddr::V6(_) => { tracker.update_stats(StatsEvent::Udp6BadRequest, 1); } } - UdpServer::handle_udp_error(ServerError::BadRequest, TransactionId(0)).await + TransactionId(0) } - } + }; + + UdpServer::handle_udp_error(ServerError::BadRequest, transaction_id).await } #[tracing::instrument(level = "debug")] @@ -181,20 +223,20 @@ impl UdpServer { let sentry = sentry::TransactionContext::new("udp server", "handle packet"); let transaction = sentry::start_transaction(sentry); - match request { + let result = match request { Request::Connect(connect_request) => { - transaction.finish(); UdpServer::handle_udp_connect(remote_addr, &connect_request, tracker).await } Request::Announce(announce_request) => { - transaction.finish(); UdpServer::handle_udp_announce(remote_addr, &announce_request, tracker).await } Request::Scrape(scrape_request) => { - transaction.finish(); UdpServer::handle_udp_scrape(remote_addr, &scrape_request, tracker).await } - } + }; + + transaction.finish(); + result } #[tracing::instrument(level = "debug")] @@ -204,78 +246,85 @@ impl UdpServer { transaction_id: request.transaction_id, connection_id }); - match remote_addr { - SocketAddr::V4(_) => { - tracker.update_stats(StatsEvent::Udp4ConnectionsHandled, 1); - } - SocketAddr::V6(_) => { - tracker.update_stats(StatsEvent::Udp6ConnectionsHandled, 1); - } + + let stats_event = if remote_addr.is_ipv4() { + StatsEvent::Udp4ConnectionsHandled + } else { + StatsEvent::Udp6ConnectionsHandled }; + tracker.update_stats(stats_event, 1); + Ok(response) } #[tracing::instrument(level = "debug")] pub async fn handle_udp_announce(remote_addr: SocketAddr, request: &AnnounceRequest, tracker: Arc) -> Result { - if tracker.config.tracker_config.clone().whitelist_enabled && !tracker.check_whitelist(InfoHash(request.info_hash.0)) { + let config = tracker.config.tracker_config.clone(); + + if config.whitelist_enabled && !tracker.check_whitelist(InfoHash(request.info_hash.0)) { debug!("[UDP ERROR] Torrent Not Whitelisted"); return Err(ServerError::TorrentNotWhitelisted); } - if tracker.config.tracker_config.clone().blacklist_enabled && tracker.check_blacklist(InfoHash(request.info_hash.0)) { + if config.blacklist_enabled && tracker.check_blacklist(InfoHash(request.info_hash.0)) { debug!("[UDP ERROR] Torrent Blacklisted"); return Err(ServerError::TorrentBlacklisted); } - if tracker.config.tracker_config.clone().keys_enabled { + + if config.keys_enabled { if request.path.len() < 50 { debug!("[UDP ERROR] Unknown Key"); return Err(ServerError::UnknownKey); } let key_path_extract = &request.path[10..50]; - match hex::decode(key_path_extract) { - Ok(result) => { - let key = <[u8; 20]>::try_from(result[0..20].as_ref()).unwrap(); + if let Ok(result) = hex::decode(key_path_extract) { + if result.len() >= 20 { + let key = <[u8; 20]>::try_from(&result[0..20]).unwrap(); if !tracker.check_key(InfoHash::from(key)) { debug!("[UDP ERROR] Unknown Key"); return Err(ServerError::UnknownKey); } - } - Err(_) => { - debug!("[UDP ERROR] Unknown Key"); + } else { + debug!("[UDP ERROR] Unknown Key - insufficient bytes"); return Err(ServerError::UnknownKey); } + } else { + debug!("[UDP ERROR] Unknown Key"); + return Err(ServerError::UnknownKey); } } - let mut user_key: Option = None; - if tracker.config.tracker_config.clone().users_enabled { - let mut user_key_path_extract = None; - if tracker.config.tracker_config.clone().users_enabled && request.path.len() >= 91 { - user_key_path_extract = Some(&request.path[51..=91]); - } - if !tracker.config.tracker_config.clone().users_enabled && request.path.len() >= 50 { - user_key_path_extract = Some(&request.path[10..=50]) - } - if user_key_path_extract.is_some() { - match hex::decode(user_key_path_extract.unwrap()) { - Ok(result) => { - let key = <[u8; 20]>::try_from(result[0..20].as_ref()).unwrap(); - user_key = match tracker.check_user_key(UserId::from(key)) { - None => { - debug!("[UDP ERROR] Peer Key Not Valid"); - return Err(ServerError::PeerKeyNotValid); - } - Some(user_id) => { - Some(user_id) - } - }; + + let user_key = if config.users_enabled { + let user_key_path_extract = if request.path.len() >= 91 { + Some(&request.path[51..=91]) + } else if !config.users_enabled && request.path.len() >= 50 { + Some(&request.path[10..=50]) + } else { + None + }; + + if let Some(path) = user_key_path_extract { + match hex::decode(path) { + Ok(result) if result.len() >= 20 => { + let key = <[u8; 20]>::try_from(&result[0..20]).unwrap(); + tracker.check_user_key(UserId::from(key)) } - Err(error) => { - debug!("[UDP ERROR] Hex Decode Error"); - debug!("{error:#?}"); + _ => { + debug!("[UDP ERROR] Peer Key Not Valid"); return Err(ServerError::PeerKeyNotValid); } } + } else { + None } + } else { + None + }; + + if config.users_enabled && user_key.is_none() { + debug!("[UDP ERROR] Peer Key Not Valid"); + return Err(ServerError::PeerKeyNotValid); } + let torrent = match tracker.handle_announce(tracker.clone(), AnnounceQueryRequest { info_hash: InfoHash(request.info_hash.0), peer_id: PeerId(request.peer_id.0), @@ -289,138 +338,114 @@ impl UdpServer { remote_addr: remote_addr.ip(), numwant: request.peers_wanted.0 as u64, }, user_key).await { - Ok(result) => { result.1 } + Ok(result) => result.1, Err(error) => { - debug!("[UDP ERROR] Handle Announce - Internal Server Error"); - debug!("{error:#?}"); + debug!("[UDP ERROR] Handle Announce - Internal Server Error: {error:#?}"); return Err(ServerError::InternalServerError); } }; + let torrent_peers = tracker.get_torrent_peers(request.info_hash, 72, TorrentPeersType::All, Some(remote_addr.ip())); - let mut peers: Vec> = Vec::new(); - let mut peers6: Vec> = Vec::new(); - let mut count = 0; - if request.bytes_left.0 as u64 != 0 { - if remote_addr.is_ipv4() { - match torrent_peers { - None => {} - Some(ref torrent_peers_unwrapped) => { - for (_, torrent_peer) in torrent_peers_unwrapped.seeds_ipv4.iter() { - if count > 72 { - break; - } - peers.push(ResponsePeer:: { - ip_address: torrent_peer.peer_addr.ip().to_string().parse::().unwrap(), - port: Port(torrent_peer.peer_addr.port()), - }); + + let (peers, peers6) = if let Some(torrent_peers_unwrapped) = torrent_peers { + let mut peers = Vec::with_capacity(72); + let mut peers6 = Vec::with_capacity(72); + let mut count = 0; + + if request.bytes_left.0 != 0 { + if remote_addr.is_ipv4() { + for torrent_peer in torrent_peers_unwrapped.seeds_ipv4.values().take(72) { + if count >= 72 { break; } + if let Ok(ip) = torrent_peer.peer_addr.ip().to_string().parse::() { + peers.push(ResponsePeer { ip_address: ip, port: Port(torrent_peer.peer_addr.port()) }); count += 1; } } - } - } else { - match torrent_peers { - None => {} - Some(ref torrent_peers_unwrapped) => { - for (_, torrent_peer) in torrent_peers_unwrapped.seeds_ipv6.iter() { - if count > 72 { - break; - } - peers6.push(ResponsePeer:: { - ip_address: torrent_peer.peer_addr.ip().to_string().parse::().unwrap(), - port: Port(torrent_peer.peer_addr.port()), - }); + } else { + for torrent_peer in torrent_peers_unwrapped.seeds_ipv6.values().take(72) { + if count >= 72 { break; } + if let Ok(ip) = torrent_peer.peer_addr.ip().to_string().parse::() { + peers6.push(ResponsePeer { ip_address: ip, port: Port(torrent_peer.peer_addr.port()) }); count += 1; } } } } - } - if remote_addr.is_ipv4() { - match torrent_peers { - None => {} - Some(ref torrent_peers_unwrapped) => { - for (_, torrent_peer) in torrent_peers_unwrapped.peers_ipv4.iter() { - if count > 72 { - break; - } - peers.push(ResponsePeer:: { - ip_address: torrent_peer.peer_addr.ip().to_string().parse::().unwrap(), - port: Port(torrent_peer.peer_addr.port()), - }); - count += 1; + + if remote_addr.is_ipv4() { + for torrent_peer in torrent_peers_unwrapped.peers_ipv4.values().take(72 - count) { + if let std::net::IpAddr::V4(ip) = torrent_peer.peer_addr.ip() { + peers.push(ResponsePeer { ip_address: ip, port: Port(torrent_peer.peer_addr.port()) }); } } - } - } else { - match torrent_peers { - None => {} - Some(ref torrent_peers_unwrapped) => { - for (_, torrent_peer) in torrent_peers_unwrapped.peers_ipv6.iter() { - if count > 72 { - break; - } - peers6.push(ResponsePeer:: { - ip_address: torrent_peer.peer_addr.ip().to_string().parse::().unwrap(), - port: Port(torrent_peer.peer_addr.port()), - }); - count += 1; + } else { + for torrent_peer in torrent_peers_unwrapped.peers_ipv6.values().take(72 - count) { + if let std::net::IpAddr::V6(ip) = torrent_peer.peer_addr.ip() { + peers6.push(ResponsePeer { ip_address: ip, port: Port(torrent_peer.peer_addr.port()) }); } } } - } - let mut announce_response = Response::from(AnnounceResponse { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(tracker.config.tracker_config.clone().request_interval as i32), - leechers: NumberOfPeers(torrent.peers.len() as i32), - seeders: NumberOfPeers(torrent.seeds.len() as i32), - peers, - }); - if remote_addr.is_ipv6() { - announce_response = Response::from(AnnounceResponse { + (peers, peers6) + } else { + (Vec::new(), Vec::new()) + }; + + let response = if remote_addr.is_ipv6() { + Response::from(AnnounceResponse { transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(tracker.config.tracker_config.clone().request_interval as i32), + announce_interval: AnnounceInterval(config.request_interval as i32), leechers: NumberOfPeers(torrent.peers.len() as i32), seeders: NumberOfPeers(torrent.seeds.len() as i32), - peers: peers6 - }); - } - if remote_addr.is_ipv4() { - tracker.update_stats(StatsEvent::Udp4AnnouncesHandled, 1); + peers: peers6, + }) } else { - tracker.update_stats(StatsEvent::Udp6AnnouncesHandled, 1); - } - Ok(announce_response) + Response::from(AnnounceResponse { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval(config.request_interval as i32), + leechers: NumberOfPeers(torrent.peers.len() as i32), + seeders: NumberOfPeers(torrent.seeds.len() as i32), + peers, + }) + }; + + let stats_event = if remote_addr.is_ipv4() { + StatsEvent::Udp4AnnouncesHandled + } else { + StatsEvent::Udp6AnnouncesHandled + }; + tracker.update_stats(stats_event, 1); + + Ok(response) } #[tracing::instrument(level = "debug")] pub async fn handle_udp_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tracker: Arc) -> Result { - let mut torrent_stats: Vec = Vec::new(); - for info_hash in request.info_hashes.iter() { - let info_hash = InfoHash(info_hash.0); + let mut torrent_stats = Vec::with_capacity(request.info_hashes.len()); + + for info_hash in &request.info_hashes { let scrape_entry = match tracker.get_torrent(InfoHash(info_hash.0)) { - None => { - TorrentScrapeStatistics { - seeders: NumberOfPeers(0), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), - } - } - Some(torrent_info) => { - TorrentScrapeStatistics { - seeders: NumberOfPeers(torrent_info.seeds.len() as i32), - completed: NumberOfDownloads(torrent_info.completed as i32), - leechers: NumberOfPeers(torrent_info.peers.len() as i32), - } - } + Some(torrent_info) => TorrentScrapeStatistics { + seeders: NumberOfPeers(torrent_info.seeds.len() as i32), + completed: NumberOfDownloads(torrent_info.completed as i32), + leechers: NumberOfPeers(torrent_info.peers.len() as i32), + }, + None => TorrentScrapeStatistics { + seeders: NumberOfPeers(0), + completed: NumberOfDownloads(0), + leechers: NumberOfPeers(0), + }, }; torrent_stats.push(scrape_entry); } - if remote_addr.is_ipv4() { - tracker.update_stats(StatsEvent::Udp4ScrapesHandled, 1); + + let stats_event = if remote_addr.is_ipv4() { + StatsEvent::Udp4ScrapesHandled } else { - tracker.update_stats(StatsEvent::Udp6ScrapesHandled, 1); - } + StatsEvent::Udp6ScrapesHandled + }; + tracker.update_stats(stats_event, 1); + Ok(Response::from(ScrapeResponse { transaction_id: request.transaction_id, torrent_stats, @@ -429,7 +454,9 @@ impl UdpServer { #[tracing::instrument(level = "debug")] pub async fn handle_udp_error(e: ServerError, transaction_id: TransactionId) -> Response { - let message = e.to_string(); - Response::from(ErrorResponse { transaction_id, message: message.into() }) + Response::from(ErrorResponse { + transaction_id, + message: e.to_string().into() + }) } } \ No newline at end of file diff --git a/src/udp/structs.rs b/src/udp/structs.rs index cdaf220..26545f8 100644 --- a/src/udp/structs.rs +++ b/src/udp/structs.rs @@ -14,4 +14,6 @@ pub mod number_of_downloads; pub mod port; pub mod peer_key; pub mod response_peer; -pub mod udp_server; \ No newline at end of file +pub mod udp_server; +pub mod parse_pool; +pub mod udp_packet; diff --git a/src/udp/structs/parse_pool.rs b/src/udp/structs/parse_pool.rs new file mode 100644 index 0000000..755382a --- /dev/null +++ b/src/udp/structs/parse_pool.rs @@ -0,0 +1,7 @@ +use std::sync::Arc; +use crate::udp::structs::udp_packet::UdpPacket; +use crossbeam::queue::ArrayQueue; + +pub struct ParsePool { + pub payload: Arc> +} \ No newline at end of file diff --git a/src/udp/structs/udp_packet.rs b/src/udp/structs/udp_packet.rs new file mode 100644 index 0000000..359988a --- /dev/null +++ b/src/udp/structs/udp_packet.rs @@ -0,0 +1,12 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use crate::udp::udp::MAX_PACKET_SIZE; + +#[derive(Debug, Clone)] +pub struct UdpPacket { + pub remote_addr: SocketAddr, + pub data: [u8; MAX_PACKET_SIZE], + pub data_len: usize, + pub socket: Arc, +} \ No newline at end of file diff --git a/src/udp/structs/udp_server.rs b/src/udp/structs/udp_server.rs index 73a6f0d..e380117 100644 --- a/src/udp/structs/udp_server.rs +++ b/src/udp/structs/udp_server.rs @@ -5,6 +5,7 @@ use crate::tracker::structs::torrent_tracker::TorrentTracker; #[derive(Debug)] pub struct UdpServer { pub(crate) socket: Arc, - pub(crate) threads: u64, + pub(crate) udp_threads: usize, + pub(crate) worker_threads: usize, pub(crate) tracker: Arc, } \ No newline at end of file diff --git a/src/udp/udp.rs b/src/udp/udp.rs index f2bc741..9dc9def 100644 --- a/src/udp/udp.rs +++ b/src/udp/udp.rs @@ -11,14 +11,15 @@ pub const PROTOCOL_IDENTIFIER: i64 = 4_497_486_125_440; pub const MAX_SCRAPE_TORRENTS: u8 = 74; pub const MAX_PACKET_SIZE: usize = 1496; -pub async fn udp_service(addr: SocketAddr, threads: u64, recv_buffer_size: usize, send_buffer_size: usize, reuse_address: bool, data: Arc, rx: tokio::sync::watch::Receiver, tokio_udp: Arc) -> JoinHandle<()> +#[allow(clippy::too_many_arguments)] +pub async fn udp_service(addr: SocketAddr, udp_threads: usize, worker_threads: usize, recv_buffer_size: usize, send_buffer_size: usize, reuse_address: bool, data: Arc, rx: tokio::sync::watch::Receiver, tokio_udp: Arc) -> JoinHandle<()> { - let udp_server = UdpServer::new(data, addr, threads, recv_buffer_size, send_buffer_size, reuse_address).await.unwrap_or_else(|e| { + let udp_server = UdpServer::new(data, addr, udp_threads, worker_threads, recv_buffer_size, send_buffer_size, reuse_address).await.unwrap_or_else(|e| { error!("Could not listen to the UDP port: {e}"); exit(1); }); - info!("[UDP] Starting server listener on {addr} with {threads} threads"); + info!("[UDP] Starting a server listener on {addr} with {udp_threads} UDP threads and {worker_threads} worker threads"); tokio_udp.spawn(async move { udp_server.start(rx).await; })