Merge pull request #8 from greatest-ape/access-list

Add access list support, refactor aquatic_ws_protocol, add some optimizations, update dependencies
This commit is contained in:
Joakim Frostegård 2021-10-17 22:08:18 +02:00 committed by GitHub
commit d0b8ed3f64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1444 additions and 972 deletions

178
Cargo.lock generated
View file

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.16.0"
@ -23,9 +25,9 @@ checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
[[package]]
name = "ahash"
version = "0.7.4"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
@ -43,9 +45,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "aquatic"
@ -72,8 +74,13 @@ dependencies = [
name = "aquatic_common"
version = "0.1.0"
dependencies = [
"anyhow",
"arc-swap",
"hashbrown 0.11.2",
"hex",
"indexmap",
"rand",
"serde",
]
[[package]]
@ -275,6 +282,12 @@ dependencies = [
"tungstenite",
]
[[package]]
name = "arc-swap"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
[[package]]
name = "arrayvec"
version = "0.4.12"
@ -350,9 +363,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
@ -362,9 +375,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.7.0"
version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
[[package]]
name = "byteorder"
@ -389,9 +402,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.70"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]]
name = "cfg-if"
@ -425,22 +438,22 @@ dependencies = [
[[package]]
name = "console"
version = "0.14.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
dependencies = [
"encode_unicode",
"lazy_static",
"libc",
"once_cell",
"terminal_size",
"winapi",
]
[[package]]
name = "core-foundation"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
dependencies = [
"core-foundation-sys",
"libc",
@ -448,9 +461,9 @@ dependencies = [
[[package]]
name = "core-foundation-sys"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
@ -685,9 +698,9 @@ checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "half"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
checksum = "ac5956d4e63858efaec57e0d6c1c2f6a41e1487f830314a324ccd7e2223a7ca0"
[[package]]
name = "halfbrown"
@ -715,7 +728,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash 0.7.4",
"ahash 0.7.6",
"serde",
]
@ -742,9 +755,9 @@ checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669"
[[package]]
name = "http"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
dependencies = [
"bytes",
"fnv",
@ -792,9 +805,9 @@ dependencies = [
[[package]]
name = "instant"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
dependencies = [
"cfg-if",
]
@ -816,9 +829,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "js-sys"
version = "0.3.53"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [
"wasm-bindgen",
]
@ -831,9 +844,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.101"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "libm"
@ -950,14 +963,15 @@ dependencies = [
[[package]]
name = "nix"
version = "0.19.1"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
dependencies = [
"bitflags",
"cc",
"cfg-if",
"libc",
"memoffset",
]
[[package]]
@ -1070,9 +1084,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]]
name = "openssl-sys"
version = "0.9.66"
version = "0.9.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
dependencies = [
"autocfg",
"cc",
@ -1114,9 +1128,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pkg-config"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]]
name = "plotters"
@ -1148,15 +1162,15 @@ dependencies = [
[[package]]
name = "ppv-lite86"
version = "0.2.10"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
[[package]]
name = "privdrop"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd4c2739642e70439d1c0d9545beec45c1e54128739b3cda29bf2c366028c87"
checksum = "4c02cf257b10e4b807bccadb19630d5dea7e0369c3c5e84673ee8e58dc8da6a5"
dependencies = [
"libc",
"nix",
@ -1164,9 +1178,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.29"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [
"unicode-xid",
]
@ -1195,9 +1209,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
@ -1235,9 +1249,9 @@ dependencies = [
[[package]]
name = "rand_distr"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051b398806e42b9cd04ad9ec8f81e355d0a382c543ac6672c62f5a5b452ef142"
checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f"
dependencies = [
"num-traits",
"rand",
@ -1444,9 +1458,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
@ -1468,9 +1482,9 @@ dependencies = [
[[package]]
name = "simd-json"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529edb21cdc2629d7214de58672ec9fe5678b623e8fffb03327f77d7291d8865"
checksum = "e67eb096671712144fb1357787c4312720c99444f52900ca2a20bee57a02cc64"
dependencies = [
"halfbrown",
"serde",
@ -1487,9 +1501,9 @@ checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559"
[[package]]
name = "simplelog"
version = "0.10.0"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959"
dependencies = [
"chrono",
"log",
@ -1498,15 +1512,15 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "smallvec"
version = "1.6.1"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "smartstring"
@ -1519,9 +1533,9 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
dependencies = [
"libc",
"winapi",
@ -1535,9 +1549,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.76"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [
"proc-macro2",
"quote",
@ -1546,9 +1560,9 @@ dependencies = [
[[package]]
name = "synstructure"
version = "0.12.5"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
@ -1600,18 +1614,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.29"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.29"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
@ -1640,9 +1654,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.3.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [
"tinyvec_macros",
]
@ -1689,9 +1703,9 @@ checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
[[package]]
name = "unicode-bidi"
version = "0.3.6"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-normalization"
@ -1704,9 +1718,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
@ -1781,9 +1795,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.76"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@ -1791,9 +1805,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.76"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [
"bumpalo",
"lazy_static",
@ -1806,9 +1820,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.76"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -1816,9 +1830,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.76"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [
"proc-macro2",
"quote",
@ -1829,15 +1843,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.76"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "web-sys"
version = "0.3.53"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [
"js-sys",
"wasm-bindgen",

View file

@ -65,7 +65,15 @@ The configuration file values you will most likely want to adjust are
`address` under the `network` section (listening address). This goes for all
three protocols.
Some documentation of the various options might be available under
Access control by info hash is supported for all protocols. Relevant part of configuration:
```toml
[access_list]
mode = 'off' # Change to 'black' (blacklist) or 'white' (whitelist)
path = '' # Path to text file with newline-delimited hex-encoded info hashes
```
Some more documentation of configuration file values might be available under
`src/lib/config.rs` in crates `aquatic_udp`, `aquatic_http`, `aquatic_ws`.
## Details on implementations

25
TODO.md
View file

@ -1,13 +1,23 @@
# TODO
* Consider turning on safety and override flags in mimalloc (mostly for
simd-json)
* access lists:
* use arc-swap Cache
* test functionality
* aquatic_udp
* aquatic_http
* aquatic_ws, including sending back new error responses
* aquatic_ws: should it send back error on message parse error, or does that
just indicate that not enough data has been received yet?
* Consider turning on safety and override flags in mimalloc, mostly for
simd-json. It might be faster to just stop using simd-json if I consider
it insecure, which it maybe isn't.
## General
* extract response peers: extract "one extra" to compensate for removal,
of sender if present in selection? maybe make criterion benchmark,
optimize. consider rerunning udp benchmark, last change (filtering out
sender) might have had an adverse impact on performance.
optimize
## aquatic_http_load_test
* how handle large number of peers for "popular" torrents in keepalive mode?
@ -24,7 +34,6 @@
## aquatic_http
* test torrent transfer with real clients
* test tls
* scrape: does it work (serialization etc), and with multiple hashes?
* 'left' optional in magnet requests? Probably not. Transmission sends huge
positive number.
@ -40,12 +49,6 @@
scrape requests I suppose.
## aquatic_ws
* panic when unwrapping peer_address after peer closes connection:
```
thread 'socket-01' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 22, kind: InvalidInput, message: "Invalid argument" }', aquatic_ws/src/lib/network/connection.rs:28:59
```
* websocket_max_frame_size should be at least something like 64 * 1024,
maybe put it and message size at 128k just to be sure
* test transfer, specifically ipv6/ipv4 mapping

View file

@ -11,5 +11,10 @@ repository = "https://github.com/greatest-ape/aquatic"
name = "aquatic_common"
[dependencies]
anyhow = "1"
arc-swap = "1"
hashbrown = "0.11.2"
hex = "0.4"
indexmap = "1"
rand = { version = "0.8", features = ["small_rng"] }
serde = { version = "1", features = ["derive"] }

View file

@ -0,0 +1,93 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::sync::Arc;
use arc_swap::ArcSwap;
use hashbrown::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AccessListMode {
/// Only serve torrents with info hash present in file
White,
/// Do not serve torrents if info hash present in file
Black,
/// Turn off access list functionality
Off,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AccessListConfig {
pub mode: AccessListMode,
/// Path to access list file consisting of newline-separated hex-encoded info hashes.
///
/// If using chroot mode, path must be relative to new root.
pub path: PathBuf,
}
impl Default for AccessListConfig {
fn default() -> Self {
Self {
path: "".into(),
mode: AccessListMode::Off,
}
}
}
pub struct AccessList(ArcSwap<HashSet<[u8; 20]>>);
impl Default for AccessList {
fn default() -> Self {
Self(ArcSwap::from(Arc::new(HashSet::default())))
}
}
impl AccessList {
fn parse_info_hash(line: String) -> anyhow::Result<[u8; 20]> {
let mut bytes = [0u8; 20];
hex::decode_to_slice(line, &mut bytes)?;
Ok(bytes)
}
pub fn update_from_path(&self, path: &PathBuf) -> anyhow::Result<()> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut new_list = HashSet::new();
for line in reader.lines() {
new_list.insert(Self::parse_info_hash(line?)?);
}
self.0.store(Arc::new(new_list));
Ok(())
}
pub fn allows(&self, list_mode: AccessListMode, info_hash_bytes: &[u8; 20]) -> bool {
match list_mode {
AccessListMode::White => self.0.load().contains(info_hash_bytes),
AccessListMode::Black => !self.0.load().contains(info_hash_bytes),
AccessListMode::Off => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_info_hash() {
let f = AccessList::parse_info_hash;
assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeee".into()).is_ok());
assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeeef".into()).is_err());
assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeee".into()).is_err());
assert!(f("aaaabbbbccccddddeeeeaaaabbbbccccddddeeeö".into()).is_err());
}
}

View file

@ -4,6 +4,8 @@ use std::time::{Duration, Instant};
use indexmap::IndexMap;
use rand::Rng;
pub mod access_list;
/// Peer or connection valid until this instant
///
/// Used instead of "last seen" or similar to hopefully prevent arithmetic

View file

@ -1,6 +1,8 @@
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::Instant;
use aquatic_common::access_list::AccessList;
use crossbeam_channel::{Receiver, Sender};
use either::Either;
use hashbrown::HashMap;
@ -16,6 +18,8 @@ use aquatic_http_protocol::common::*;
use aquatic_http_protocol::request::Request;
use aquatic_http_protocol::response::{Response, ResponsePeer};
use crate::config::Config;
pub const LISTENER_TOKEN: Token = Token(0);
pub const CHANNEL_TOKEN: Token = Token(1);
@ -113,14 +117,62 @@ pub struct TorrentMaps {
pub ipv6: TorrentMap<Ipv6Addr>,
}
impl TorrentMaps {
pub fn clean(&mut self, config: &Config, access_list: &Arc<AccessList>) {
Self::clean_torrent_map(config, access_list, &mut self.ipv4);
Self::clean_torrent_map(config, access_list, &mut self.ipv6);
}
fn clean_torrent_map<I: Ip>(
config: &Config,
access_list: &Arc<AccessList>,
torrent_map: &mut TorrentMap<I>,
) {
let now = Instant::now();
torrent_map.retain(|info_hash, torrent_data| {
if !access_list.allows(config.access_list.mode, &info_hash.0) {
return false;
}
let num_seeders = &mut torrent_data.num_seeders;
let num_leechers = &mut torrent_data.num_leechers;
torrent_data.peers.retain(|_, peer| {
let keep = peer.valid_until.0 >= now;
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
}
keep
});
!torrent_data.peers.is_empty()
});
torrent_map.shrink_to_fit();
}
}
#[derive(Clone)]
pub struct State {
pub access_list: Arc<AccessList>,
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
}
impl Default for State {
fn default() -> Self {
Self {
access_list: Arc::new(Default::default()),
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
}
}

View file

@ -1,5 +1,6 @@
use std::net::SocketAddr;
use aquatic_common::access_list::AccessListConfig;
use serde::{Deserialize, Serialize};
use aquatic_cli_helpers::LogLevel;
@ -21,6 +22,7 @@ pub struct Config {
pub cleaning: CleaningConfig,
pub statistics: StatisticsConfig,
pub privileges: PrivilegeConfig,
pub access_list: AccessListConfig,
}
impl aquatic_cli_helpers::Config for Config {
@ -111,6 +113,7 @@ impl Default for Config {
cleaning: CleaningConfig::default(),
statistics: StatisticsConfig::default(),
privileges: PrivilegeConfig::default(),
access_list: AccessListConfig::default(),
}
}
}

View file

@ -22,12 +22,16 @@ pub const APP_NAME: &str = "aquatic_http: HTTP/TLS BitTorrent tracker";
pub fn run(config: Config) -> anyhow::Result<()> {
let state = State::default();
tasks::update_access_list(&config, &state);
start_workers(config.clone(), state.clone())?;
loop {
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
tasks::clean_torrents(&state);
tasks::update_access_list(&config, &state);
state.torrent_maps.lock().clean(&config, &state.access_list);
}
}
@ -51,6 +55,7 @@ pub fn start_workers(config: Config, state: State) -> anyhow::Result<()> {
for i in 0..config.socket_workers {
let config = config.clone();
let state = state.clone();
let socket_worker_statuses = socket_worker_statuses.clone();
let request_channel_sender = request_channel_sender.clone();
let opt_tls_acceptor = opt_tls_acceptor.clone();
@ -67,6 +72,7 @@ pub fn start_workers(config: Config, state: State) -> anyhow::Result<()> {
.spawn(move || {
network::run_socket_worker(
config,
state,
i,
socket_worker_statuses,
request_channel_sender,

View file

@ -3,6 +3,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use std::vec::Drain;
use aquatic_http_protocol::request::Request;
use hashbrown::HashMap;
use log::{debug, error, info};
use mio::net::TcpListener;
@ -25,6 +26,7 @@ const CONNECTION_CLEAN_INTERVAL: usize = 2 ^ 22;
pub fn run_socket_worker(
config: Config,
state: State,
socket_worker_index: usize,
socket_worker_statuses: SocketWorkerStatuses,
request_channel_sender: RequestChannelSender,
@ -38,6 +40,7 @@ pub fn run_socket_worker(
run_poll_loop(
config,
&state,
socket_worker_index,
request_channel_sender,
response_channel_receiver,
@ -55,6 +58,7 @@ pub fn run_socket_worker(
pub fn run_poll_loop(
config: Config,
state: &State,
socket_worker_index: usize,
request_channel_sender: RequestChannelSender,
response_channel_receiver: ResponseChannelReceiver,
@ -100,6 +104,7 @@ pub fn run_poll_loop(
} else if token != CHANNEL_TOKEN {
handle_connection_read_event(
&config,
&state,
socket_worker_index,
&mut poll,
&request_channel_sender,
@ -179,6 +184,7 @@ fn accept_new_streams(
/// then read requests and pass on through channel.
pub fn handle_connection_read_event(
config: &Config,
state: &State,
socket_worker_index: usize,
poll: &mut Poll,
request_channel_sender: &RequestChannelSender,
@ -187,6 +193,7 @@ pub fn handle_connection_read_event(
poll_token: Token,
) {
let valid_until = ValidUntil::new(config.cleaning.max_connection_age);
let access_list_mode = config.access_list.mode;
loop {
// Get connection, updating valid_until
@ -203,6 +210,22 @@ pub fn handle_connection_read_event(
if let Some(established) = connection.get_established() {
match established.read_request() {
Ok(Request::Announce(ref r))
if !state.access_list.allows(access_list_mode, &r.info_hash.0) =>
{
let meta = ConnectionMeta {
worker_index: socket_worker_index,
poll_token,
peer_addr: established.peer_addr,
};
let response = FailureResponse::new("Info hash not allowed");
debug!("read disallowed request, sending back error response");
local_responses.push((meta, Response::Failure(response)));
break;
}
Ok(request) => {
let meta = ConnectionMeta {
worker_index: socket_worker_index,
@ -210,7 +233,7 @@ pub fn handle_connection_read_event(
peer_addr: established.peer_addr,
};
debug!("read request, sending to handler");
debug!("read allowed request, sending on to channel");
if let Err(err) = request_channel_sender.send((meta, request)) {
error!("RequestChannelSender: couldn't send message: {:?}", err);
@ -233,9 +256,7 @@ pub fn handle_connection_read_event(
peer_addr: established.peer_addr,
};
let response = FailureResponse {
failure_reason: "invalid request".to_string(),
};
let response = FailureResponse::new("Invalid request");
local_responses.push((meta, Response::Failure(response)));

View file

@ -1,45 +1,18 @@
use std::time::Instant;
use histogram::Histogram;
use crate::common::*;
use aquatic_common::access_list::AccessListMode;
pub fn clean_torrents(state: &State) {
let mut torrent_maps = state.torrent_maps.lock();
use crate::{common::*, config::Config};
clean_torrent_map(&mut torrent_maps.ipv4);
clean_torrent_map(&mut torrent_maps.ipv6);
}
fn clean_torrent_map<I: Ip>(torrent_map: &mut TorrentMap<I>) {
let now = Instant::now();
torrent_map.retain(|_, torrent_data| {
let num_seeders = &mut torrent_data.num_seeders;
let num_leechers = &mut torrent_data.num_leechers;
torrent_data.peers.retain(|_, peer| {
let keep = peer.valid_until.0 >= now;
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
pub fn update_access_list(config: &Config, state: &State) {
match config.access_list.mode {
AccessListMode::White | AccessListMode::Black => {
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
::log::error!("Couldn't update access list: {:?}", err);
}
keep
});
!torrent_data.peers.is_empty()
});
torrent_map.shrink_to_fit();
}
AccessListMode::Off => {}
}
}
pub fn print_statistics(state: &State) {

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::Write;
use std::net::{Ipv4Addr, Ipv6Addr};
@ -131,10 +132,16 @@ impl ScrapeResponse {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailureResponse {
#[serde(rename = "failure reason")]
pub failure_reason: String,
pub failure_reason: Cow<'static, str>,
}
impl FailureResponse {
pub fn new<S: Into<Cow<'static, str>>>(reason: S) -> Self {
Self {
failure_reason: reason.into(),
}
}
fn write<W: Write>(&self, output: &mut W) -> ::std::io::Result<usize> {
let mut bytes_written = 0usize;
@ -242,7 +249,7 @@ impl quickcheck::Arbitrary for ScrapeResponse {
impl quickcheck::Arbitrary for FailureResponse {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
Self {
failure_reason: String::arbitrary(g),
failure_reason: String::arbitrary(g).into(),
}
}
}

View file

@ -1,14 +1,17 @@
use std::hash::Hash;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::{atomic::AtomicUsize, Arc};
use std::time::Instant;
use hashbrown::HashMap;
use indexmap::IndexMap;
use parking_lot::Mutex;
pub use aquatic_common::ValidUntil;
pub use aquatic_common::{access_list::AccessList, ValidUntil};
pub use aquatic_udp_protocol::*;
use crate::config::Config;
pub const MAX_PACKET_SIZE: usize = 4096;
pub trait Ip: Hash + PartialEq + Eq + Clone + Copy {
@ -33,6 +36,15 @@ pub struct ConnectionKey {
pub socket_addr: SocketAddr,
}
impl ConnectionKey {
pub fn new(connection_id: ConnectionId, socket_addr: SocketAddr) -> Self {
Self {
connection_id,
socket_addr,
}
}
}
pub type ConnectionMap = HashMap<ConnectionKey, ValidUntil>;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
@ -108,6 +120,54 @@ pub struct TorrentMaps {
pub ipv6: TorrentMap<Ipv6Addr>,
}
impl TorrentMaps {
/// Remove disallowed and inactive torrents
pub fn clean(&mut self, config: &Config, access_list: &Arc<AccessList>) {
let now = Instant::now();
let access_list_mode = config.access_list.mode;
self.ipv4.retain(|info_hash, torrent| {
access_list.allows(access_list_mode, &info_hash.0)
&& Self::clean_torrent_and_peers(now, torrent)
});
self.ipv4.shrink_to_fit();
self.ipv6.retain(|info_hash, torrent| {
access_list.allows(access_list_mode, &info_hash.0)
&& Self::clean_torrent_and_peers(now, torrent)
});
self.ipv6.shrink_to_fit();
}
/// Returns true if torrent is to be kept
#[inline]
fn clean_torrent_and_peers<I: Ip>(now: Instant, torrent: &mut TorrentData<I>) -> bool {
let num_seeders = &mut torrent.num_seeders;
let num_leechers = &mut torrent.num_leechers;
torrent.peers.retain(|_, peer| {
let keep = peer.valid_until.0 > now;
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
}
keep
});
!torrent.peers.is_empty()
}
}
#[derive(Default)]
pub struct Statistics {
pub requests_received: AtomicUsize,
@ -119,6 +179,7 @@ pub struct Statistics {
#[derive(Clone)]
pub struct State {
pub access_list: Arc<AccessList>,
pub connections: Arc<Mutex<ConnectionMap>>,
pub torrents: Arc<Mutex<TorrentMaps>>,
pub statistics: Arc<Statistics>,
@ -127,6 +188,7 @@ pub struct State {
impl Default for State {
fn default() -> Self {
Self {
access_list: Arc::new(AccessList::default()),
connections: Arc::new(Mutex::new(HashMap::new())),
torrents: Arc::new(Mutex::new(TorrentMaps::default())),
statistics: Arc::new(Statistics::default()),

View file

@ -1,5 +1,6 @@
use std::net::SocketAddr;
use aquatic_common::access_list::AccessListConfig;
use serde::{Deserialize, Serialize};
use aquatic_cli_helpers::LogLevel;
@ -21,6 +22,7 @@ pub struct Config {
pub statistics: StatisticsConfig,
pub cleaning: CleaningConfig,
pub privileges: PrivilegeConfig,
pub access_list: AccessListConfig,
}
impl aquatic_cli_helpers::Config for Config {
@ -113,6 +115,7 @@ impl Default for Config {
statistics: StatisticsConfig::default(),
cleaning: CleaningConfig::default(),
privileges: PrivilegeConfig::default(),
access_list: AccessListConfig::default(),
}
}
}

View file

@ -1,436 +0,0 @@
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use std::vec::Drain;
use crossbeam_channel::{Receiver, Sender};
use parking_lot::MutexGuard;
use rand::{
rngs::{SmallRng, StdRng},
Rng, SeedableRng,
};
use aquatic_common::{convert_ipv4_mapped_ipv6, extract_response_peers};
use aquatic_udp_protocol::*;
use crate::common::*;
use crate::config::Config;
pub fn run_request_worker(
state: State,
config: Config,
request_receiver: Receiver<(Request, SocketAddr)>,
response_sender: Sender<(Response, SocketAddr)>,
) {
let mut connect_requests: Vec<(ConnectRequest, SocketAddr)> = Vec::new();
let mut announce_requests: Vec<(AnnounceRequest, SocketAddr)> = Vec::new();
let mut scrape_requests: Vec<(ScrapeRequest, SocketAddr)> = Vec::new();
let mut responses: Vec<(Response, SocketAddr)> = Vec::new();
let mut std_rng = StdRng::from_entropy();
let mut small_rng = SmallRng::from_rng(&mut std_rng).unwrap();
let timeout = Duration::from_micros(config.handlers.channel_recv_timeout_microseconds);
loop {
let mut opt_connections = None;
// Collect requests from channel, divide them by type
//
// Collect a maximum number of request. Stop collecting before that
// number is reached if having waited for too long for a request, but
// only if ConnectionMap mutex isn't locked.
for i in 0..config.handlers.max_requests_per_iter {
let (request, src): (Request, SocketAddr) = if i == 0 {
match request_receiver.recv() {
Ok(r) => r,
Err(_) => break, // Really shouldn't happen
}
} else {
match request_receiver.recv_timeout(timeout) {
Ok(r) => r,
Err(_) => {
if let Some(guard) = state.connections.try_lock() {
opt_connections = Some(guard);
break;
} else {
continue;
}
}
}
};
match request {
Request::Connect(r) => connect_requests.push((r, src)),
Request::Announce(r) => announce_requests.push((r, src)),
Request::Scrape(r) => scrape_requests.push((r, src)),
}
}
let mut connections: MutexGuard<ConnectionMap> =
opt_connections.unwrap_or_else(|| state.connections.lock());
handle_connect_requests(
&config,
&mut connections,
&mut std_rng,
connect_requests.drain(..),
&mut responses,
);
announce_requests.retain(|(request, src)| {
let connection_key = ConnectionKey {
connection_id: request.connection_id,
socket_addr: *src,
};
if connections.contains_key(&connection_key) {
true
} else {
let response = ErrorResponse {
transaction_id: request.transaction_id,
message: "Connection invalid or expired".to_string(),
};
responses.push((response.into(), *src));
false
}
});
scrape_requests.retain(|(request, src)| {
let connection_key = ConnectionKey {
connection_id: request.connection_id,
socket_addr: *src,
};
if connections.contains_key(&connection_key) {
true
} else {
let response = ErrorResponse {
transaction_id: request.transaction_id,
message: "Connection invalid or expired".to_string(),
};
responses.push((response.into(), *src));
false
}
});
::std::mem::drop(connections);
if !(announce_requests.is_empty() && scrape_requests.is_empty()) {
let mut torrents = state.torrents.lock();
handle_announce_requests(
&config,
&mut torrents,
&mut small_rng,
announce_requests.drain(..),
&mut responses,
);
handle_scrape_requests(&mut torrents, scrape_requests.drain(..), &mut responses);
}
for r in responses.drain(..) {
if let Err(err) = response_sender.send(r) {
::log::error!("error sending response to channel: {}", err);
}
}
}
}
#[inline]
pub fn handle_connect_requests(
config: &Config,
connections: &mut MutexGuard<ConnectionMap>,
rng: &mut StdRng,
requests: Drain<(ConnectRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let valid_until = ValidUntil::new(config.cleaning.max_connection_age);
responses.extend(requests.map(|(request, src)| {
let connection_id = ConnectionId(rng.gen());
let key = ConnectionKey {
connection_id,
socket_addr: src,
};
connections.insert(key, valid_until);
let response = Response::Connect(ConnectResponse {
connection_id,
transaction_id: request.transaction_id,
});
(response, src)
}));
}
#[inline]
pub fn handle_announce_requests(
config: &Config,
torrents: &mut MutexGuard<TorrentMaps>,
rng: &mut SmallRng,
requests: Drain<(AnnounceRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let peer_valid_until = ValidUntil::new(config.cleaning.max_peer_age);
responses.extend(requests.map(|(request, src)| {
let peer_ip = convert_ipv4_mapped_ipv6(src.ip());
let response = match peer_ip {
IpAddr::V4(ip) => handle_announce_request(
config,
rng,
&mut torrents.ipv4,
request,
ip,
peer_valid_until,
),
IpAddr::V6(ip) => handle_announce_request(
config,
rng,
&mut torrents.ipv6,
request,
ip,
peer_valid_until,
),
};
(response.into(), src)
}));
}
fn handle_announce_request<I: Ip>(
config: &Config,
rng: &mut SmallRng,
torrents: &mut TorrentMap<I>,
request: AnnounceRequest,
peer_ip: I,
peer_valid_until: ValidUntil,
) -> AnnounceResponse {
let peer_key = PeerMapKey {
ip: peer_ip,
peer_id: request.peer_id,
};
let peer_status = PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left);
let peer = Peer {
ip_address: peer_ip,
port: request.port,
status: peer_status,
valid_until: peer_valid_until,
};
let torrent_data = torrents.entry(request.info_hash).or_default();
let opt_removed_peer = match peer_status {
PeerStatus::Leeching => {
torrent_data.num_leechers += 1;
torrent_data.peers.insert(peer_key, peer)
}
PeerStatus::Seeding => {
torrent_data.num_seeders += 1;
torrent_data.peers.insert(peer_key, peer)
}
PeerStatus::Stopped => torrent_data.peers.remove(&peer_key),
};
match opt_removed_peer.map(|peer| peer.status) {
Some(PeerStatus::Leeching) => {
torrent_data.num_leechers -= 1;
}
Some(PeerStatus::Seeding) => {
torrent_data.num_seeders -= 1;
}
_ => {}
}
let max_num_peers_to_take = calc_max_num_peers_to_take(config, request.peers_wanted.0);
let response_peers = extract_response_peers(
rng,
&torrent_data.peers,
max_num_peers_to_take,
peer_key,
Peer::to_response_peer,
);
AnnounceResponse {
transaction_id: request.transaction_id,
announce_interval: AnnounceInterval(config.protocol.peer_announce_interval),
leechers: NumberOfPeers(torrent_data.num_leechers as i32),
seeders: NumberOfPeers(torrent_data.num_seeders as i32),
peers: response_peers,
}
}
#[inline]
pub fn handle_scrape_requests(
torrents: &mut MutexGuard<TorrentMaps>,
requests: Drain<(ScrapeRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let empty_stats = create_torrent_scrape_statistics(0, 0);
responses.extend(requests.map(|(request, src)| {
let mut stats: Vec<TorrentScrapeStatistics> = Vec::with_capacity(request.info_hashes.len());
let peer_ip = convert_ipv4_mapped_ipv6(src.ip());
if peer_ip.is_ipv4() {
for info_hash in request.info_hashes.iter() {
if let Some(torrent_data) = torrents.ipv4.get(info_hash) {
stats.push(create_torrent_scrape_statistics(
torrent_data.num_seeders as i32,
torrent_data.num_leechers as i32,
));
} else {
stats.push(empty_stats);
}
}
} else {
for info_hash in request.info_hashes.iter() {
if let Some(torrent_data) = torrents.ipv6.get(info_hash) {
stats.push(create_torrent_scrape_statistics(
torrent_data.num_seeders as i32,
torrent_data.num_leechers as i32,
));
} else {
stats.push(empty_stats);
}
}
}
let response = Response::Scrape(ScrapeResponse {
transaction_id: request.transaction_id,
torrent_stats: stats,
});
(response, src)
}));
}
#[inline]
fn calc_max_num_peers_to_take(config: &Config, peers_wanted: i32) -> usize {
if peers_wanted <= 0 {
config.protocol.max_response_peers as usize
} else {
::std::cmp::min(
config.protocol.max_response_peers as usize,
peers_wanted as usize,
)
}
}
#[inline(always)]
pub fn create_torrent_scrape_statistics(seeders: i32, leechers: i32) -> TorrentScrapeStatistics {
TorrentScrapeStatistics {
seeders: NumberOfPeers(seeders),
completed: NumberOfDownloads(0), // No implementation planned
leechers: NumberOfPeers(leechers),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::net::Ipv4Addr;
use indexmap::IndexMap;
use quickcheck::{quickcheck, TestResult};
use rand::thread_rng;
use super::*;
fn gen_peer_map_key_and_value(i: u32) -> (PeerMapKey<Ipv4Addr>, Peer<Ipv4Addr>) {
let ip_address = Ipv4Addr::from(i.to_be_bytes());
let peer_id = PeerId([0; 20]);
let key = PeerMapKey {
ip: ip_address,
peer_id,
};
let value = Peer {
ip_address,
port: Port(1),
status: PeerStatus::Leeching,
valid_until: ValidUntil::new(0),
};
(key, value)
}
#[test]
fn test_extract_response_peers() {
fn prop(data: (u16, u16)) -> TestResult {
let gen_num_peers = data.0 as u32;
let req_num_peers = data.1 as usize;
let mut peer_map: PeerMap<Ipv4Addr> = IndexMap::with_capacity(gen_num_peers as usize);
let mut opt_sender_key = None;
let mut opt_sender_peer = None;
for i in 0..gen_num_peers {
let (key, value) = gen_peer_map_key_and_value((i << 16) + i);
if i == 0 {
opt_sender_key = Some(key);
opt_sender_peer = Some(value.to_response_peer());
}
peer_map.insert(key, value);
}
let mut rng = thread_rng();
let peers = extract_response_peers(
&mut rng,
&peer_map,
req_num_peers,
opt_sender_key.unwrap_or_else(|| gen_peer_map_key_and_value(1).0),
Peer::to_response_peer,
);
// Check that number of returned peers is correct
let mut success = peers.len() <= req_num_peers;
if req_num_peers >= gen_num_peers as usize {
success &= peers.len() == gen_num_peers as usize
|| peers.len() + 1 == gen_num_peers as usize;
}
// Check that returned peers are unique (no overlap) and that sender
// isn't returned
let mut ip_addresses = HashSet::with_capacity(peers.len());
for peer in peers {
if peer == opt_sender_peer.clone().unwrap()
|| ip_addresses.contains(&peer.ip_address)
{
success = false;
break;
}
ip_addresses.insert(peer.ip_address);
}
TestResult::from_bool(success)
}
quickcheck(prop as fn((u16, u16)) -> TestResult);
}
}

View file

@ -0,0 +1,220 @@
use std::net::{IpAddr, SocketAddr};
use std::vec::Drain;
use parking_lot::MutexGuard;
use rand::rngs::SmallRng;
use aquatic_common::{convert_ipv4_mapped_ipv6, extract_response_peers};
use aquatic_udp_protocol::*;
use crate::common::*;
use crate::config::Config;
#[inline]
pub fn handle_announce_requests(
config: &Config,
torrents: &mut MutexGuard<TorrentMaps>,
rng: &mut SmallRng,
requests: Drain<(AnnounceRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let peer_valid_until = ValidUntil::new(config.cleaning.max_peer_age);
responses.extend(requests.map(|(request, src)| {
let peer_ip = convert_ipv4_mapped_ipv6(src.ip());
let response = match peer_ip {
IpAddr::V4(ip) => handle_announce_request(
config,
rng,
&mut torrents.ipv4,
request,
ip,
peer_valid_until,
),
IpAddr::V6(ip) => handle_announce_request(
config,
rng,
&mut torrents.ipv6,
request,
ip,
peer_valid_until,
),
};
(Response::Announce(response), src)
}));
}
fn handle_announce_request<I: Ip>(
config: &Config,
rng: &mut SmallRng,
torrents: &mut TorrentMap<I>,
request: AnnounceRequest,
peer_ip: I,
peer_valid_until: ValidUntil,
) -> AnnounceResponse {
let peer_key = PeerMapKey {
ip: peer_ip,
peer_id: request.peer_id,
};
let peer_status = PeerStatus::from_event_and_bytes_left(request.event, request.bytes_left);
let peer = Peer {
ip_address: peer_ip,
port: request.port,
status: peer_status,
valid_until: peer_valid_until,
};
let torrent_data = torrents.entry(request.info_hash).or_default();
let opt_removed_peer = match peer_status {
PeerStatus::Leeching => {
torrent_data.num_leechers += 1;
torrent_data.peers.insert(peer_key, peer)
}
PeerStatus::Seeding => {
torrent_data.num_seeders += 1;
torrent_data.peers.insert(peer_key, peer)
}
PeerStatus::Stopped => torrent_data.peers.remove(&peer_key),
};
match opt_removed_peer.map(|peer| peer.status) {
Some(PeerStatus::Leeching) => {
torrent_data.num_leechers -= 1;
}
Some(PeerStatus::Seeding) => {
torrent_data.num_seeders -= 1;
}
_ => {}
}
let max_num_peers_to_take = calc_max_num_peers_to_take(config, request.peers_wanted.0);
let response_peers = extract_response_peers(
rng,
&torrent_data.peers,
max_num_peers_to_take,
peer_key,
Peer::to_response_peer,
);
AnnounceResponse {
transaction_id: request.transaction_id,
announce_interval: AnnounceInterval(config.protocol.peer_announce_interval),
leechers: NumberOfPeers(torrent_data.num_leechers as i32),
seeders: NumberOfPeers(torrent_data.num_seeders as i32),
peers: response_peers,
}
}
#[inline]
fn calc_max_num_peers_to_take(config: &Config, peers_wanted: i32) -> usize {
if peers_wanted <= 0 {
config.protocol.max_response_peers as usize
} else {
::std::cmp::min(
config.protocol.max_response_peers as usize,
peers_wanted as usize,
)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::net::Ipv4Addr;
use indexmap::IndexMap;
use quickcheck::{quickcheck, TestResult};
use rand::thread_rng;
use super::*;
fn gen_peer_map_key_and_value(i: u32) -> (PeerMapKey<Ipv4Addr>, Peer<Ipv4Addr>) {
let ip_address = Ipv4Addr::from(i.to_be_bytes());
let peer_id = PeerId([0; 20]);
let key = PeerMapKey {
ip: ip_address,
peer_id,
};
let value = Peer {
ip_address,
port: Port(1),
status: PeerStatus::Leeching,
valid_until: ValidUntil::new(0),
};
(key, value)
}
#[test]
fn test_extract_response_peers() {
fn prop(data: (u16, u16)) -> TestResult {
let gen_num_peers = data.0 as u32;
let req_num_peers = data.1 as usize;
let mut peer_map: PeerMap<Ipv4Addr> = IndexMap::with_capacity(gen_num_peers as usize);
let mut opt_sender_key = None;
let mut opt_sender_peer = None;
for i in 0..gen_num_peers {
let (key, value) = gen_peer_map_key_and_value((i << 16) + i);
if i == 0 {
opt_sender_key = Some(key);
opt_sender_peer = Some(value.to_response_peer());
}
peer_map.insert(key, value);
}
let mut rng = thread_rng();
let peers = extract_response_peers(
&mut rng,
&peer_map,
req_num_peers,
opt_sender_key.unwrap_or_else(|| gen_peer_map_key_and_value(1).0),
Peer::to_response_peer,
);
// Check that number of returned peers is correct
let mut success = peers.len() <= req_num_peers;
if req_num_peers >= gen_num_peers as usize {
success &= peers.len() == gen_num_peers as usize
|| peers.len() + 1 == gen_num_peers as usize;
}
// Check that returned peers are unique (no overlap) and that sender
// isn't returned
let mut ip_addresses = HashSet::with_capacity(peers.len());
for peer in peers {
if peer == opt_sender_peer.clone().unwrap()
|| ip_addresses.contains(&peer.ip_address)
{
success = false;
break;
}
ip_addresses.insert(peer.ip_address);
}
TestResult::from_bool(success)
}
quickcheck(prop as fn((u16, u16)) -> TestResult);
}
}

View file

@ -0,0 +1,39 @@
use std::net::SocketAddr;
use std::vec::Drain;
use parking_lot::MutexGuard;
use rand::{rngs::StdRng, Rng};
use aquatic_udp_protocol::*;
use crate::common::*;
use crate::config::Config;
#[inline]
pub fn handle_connect_requests(
config: &Config,
connections: &mut MutexGuard<ConnectionMap>,
rng: &mut StdRng,
requests: Drain<(ConnectRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let valid_until = ValidUntil::new(config.cleaning.max_connection_age);
responses.extend(requests.map(|(request, src)| {
let connection_id = ConnectionId(rng.gen());
let key = ConnectionKey {
connection_id,
socket_addr: src,
};
connections.insert(key, valid_until);
let response = Response::Connect(ConnectResponse {
connection_id,
transaction_id: request.transaction_id,
});
(response, src)
}));
}

View file

@ -0,0 +1,148 @@
use std::net::SocketAddr;
use std::time::Duration;
use crossbeam_channel::{Receiver, Sender};
use parking_lot::MutexGuard;
use rand::{
rngs::{SmallRng, StdRng},
SeedableRng,
};
use aquatic_udp_protocol::*;
use crate::common::*;
use crate::config::Config;
mod announce;
mod connect;
mod scrape;
use announce::handle_announce_requests;
use connect::handle_connect_requests;
use scrape::handle_scrape_requests;
pub fn run_request_worker(
state: State,
config: Config,
request_receiver: Receiver<(Request, SocketAddr)>,
response_sender: Sender<(Response, SocketAddr)>,
) {
let mut connect_requests: Vec<(ConnectRequest, SocketAddr)> = Vec::new();
let mut announce_requests: Vec<(AnnounceRequest, SocketAddr)> = Vec::new();
let mut scrape_requests: Vec<(ScrapeRequest, SocketAddr)> = Vec::new();
let mut responses: Vec<(Response, SocketAddr)> = Vec::new();
let mut std_rng = StdRng::from_entropy();
let mut small_rng = SmallRng::from_rng(&mut std_rng).unwrap();
let timeout = Duration::from_micros(config.handlers.channel_recv_timeout_microseconds);
loop {
let mut opt_connections = None;
// Collect requests from channel, divide them by type
//
// Collect a maximum number of request. Stop collecting before that
// number is reached if having waited for too long for a request, but
// only if ConnectionMap mutex isn't locked.
for i in 0..config.handlers.max_requests_per_iter {
let (request, src): (Request, SocketAddr) = if i == 0 {
match request_receiver.recv() {
Ok(r) => r,
Err(_) => break, // Really shouldn't happen
}
} else {
match request_receiver.recv_timeout(timeout) {
Ok(r) => r,
Err(_) => {
if let Some(guard) = state.connections.try_lock() {
opt_connections = Some(guard);
break;
} else {
continue;
}
}
}
};
match request {
Request::Connect(r) => connect_requests.push((r, src)),
Request::Announce(r) => announce_requests.push((r, src)),
Request::Scrape(r) => scrape_requests.push((r, src)),
}
}
let mut connections: MutexGuard<ConnectionMap> =
opt_connections.unwrap_or_else(|| state.connections.lock());
handle_connect_requests(
&config,
&mut connections,
&mut std_rng,
connect_requests.drain(..),
&mut responses,
);
// Check announce and scrape requests for valid connections
announce_requests.retain(|(request, src)| {
let connection_valid =
connections.contains_key(&ConnectionKey::new(request.connection_id, *src));
if !connection_valid {
responses.push((
create_invalid_connection_response(request.transaction_id),
*src,
));
}
connection_valid
});
scrape_requests.retain(|(request, src)| {
let connection_valid =
connections.contains_key(&ConnectionKey::new(request.connection_id, *src));
if !connection_valid {
responses.push((
create_invalid_connection_response(request.transaction_id),
*src,
));
}
connection_valid
});
::std::mem::drop(connections);
// Generate responses for announce and scrape requests
if !(announce_requests.is_empty() && scrape_requests.is_empty()) {
let mut torrents = state.torrents.lock();
handle_announce_requests(
&config,
&mut torrents,
&mut small_rng,
announce_requests.drain(..),
&mut responses,
);
handle_scrape_requests(&mut torrents, scrape_requests.drain(..), &mut responses);
}
for r in responses.drain(..) {
if let Err(err) = response_sender.send(r) {
::log::error!("error sending response to channel: {}", err);
}
}
}
}
fn create_invalid_connection_response(transaction_id: TransactionId) -> Response {
Response::Error(ErrorResponse {
transaction_id,
message: "Connection invalid or expired".into(),
})
}

View file

@ -0,0 +1,64 @@
use std::net::SocketAddr;
use std::vec::Drain;
use parking_lot::MutexGuard;
use aquatic_common::convert_ipv4_mapped_ipv6;
use aquatic_udp_protocol::*;
use crate::common::*;
#[inline]
pub fn handle_scrape_requests(
torrents: &mut MutexGuard<TorrentMaps>,
requests: Drain<(ScrapeRequest, SocketAddr)>,
responses: &mut Vec<(Response, SocketAddr)>,
) {
let empty_stats = create_torrent_scrape_statistics(0, 0);
responses.extend(requests.map(|(request, src)| {
let mut stats: Vec<TorrentScrapeStatistics> = Vec::with_capacity(request.info_hashes.len());
let peer_ip = convert_ipv4_mapped_ipv6(src.ip());
if peer_ip.is_ipv4() {
for info_hash in request.info_hashes.iter() {
if let Some(torrent_data) = torrents.ipv4.get(info_hash) {
stats.push(create_torrent_scrape_statistics(
torrent_data.num_seeders as i32,
torrent_data.num_leechers as i32,
));
} else {
stats.push(empty_stats);
}
}
} else {
for info_hash in request.info_hashes.iter() {
if let Some(torrent_data) = torrents.ipv6.get(info_hash) {
stats.push(create_torrent_scrape_statistics(
torrent_data.num_seeders as i32,
torrent_data.num_leechers as i32,
));
} else {
stats.push(empty_stats);
}
}
}
let response = Response::Scrape(ScrapeResponse {
transaction_id: request.transaction_id,
torrent_stats: stats,
});
(response, src)
}));
}
#[inline(always)]
fn create_torrent_scrape_statistics(seeders: i32, leechers: i32) -> TorrentScrapeStatistics {
TorrentScrapeStatistics {
seeders: NumberOfPeers(seeders),
completed: NumberOfDownloads(0), // No implementation planned
leechers: NumberOfPeers(leechers),
}
}

View file

@ -23,29 +23,42 @@ pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker";
pub fn run(config: Config) -> ::anyhow::Result<()> {
let state = State::default();
tasks::update_access_list(&config, &state);
let num_bound_sockets = start_workers(config.clone(), state.clone())?;
if config.privileges.drop_privileges {
let mut counter = 0usize;
loop {
let sockets = num_bound_sockets.load(Ordering::SeqCst);
if sockets == config.socket_workers {
PrivDrop::default()
.chroot(config.privileges.chroot_path)
.user(config.privileges.user)
.chroot(config.privileges.chroot_path.clone())
.user(config.privileges.user.clone())
.apply()?;
break;
}
::std::thread::sleep(Duration::from_millis(10));
counter += 1;
if counter == 500 {
panic!("Sockets didn't bind in time for privilege drop.");
}
}
}
loop {
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
tasks::clean_connections_and_torrents(&state);
tasks::clean_connections(&state);
tasks::update_access_list(&config, &state);
state.torrents.lock().clean(&config, &state.access_list);
}
}

View file

@ -131,6 +131,8 @@ fn read_requests(
let mut requests_received: usize = 0;
let mut bytes_received: usize = 0;
let access_list_mode = config.access_list.mode;
loop {
match socket.recv_from(&mut buffer[..]) {
Ok((amt, src)) => {
@ -144,6 +146,18 @@ fn read_requests(
}
match request {
Ok(Request::Announce(AnnounceRequest {
info_hash,
transaction_id,
..
})) if !state.access_list.allows(access_list_mode, &info_hash.0) => {
let response = Response::Error(ErrorResponse {
transaction_id,
message: "Info hash not allowed".into(),
});
local_responses.push((response, src))
}
Ok(request) => {
requests.push((request, src));
}
@ -152,9 +166,9 @@ fn read_requests(
if let Some(transaction_id) = err.transaction_id {
let opt_message = if err.error.is_some() {
Some("Parse error".to_string())
Some("Parse error".into())
} else if let Some(message) = err.message {
Some(message)
Some(message.into())
} else {
None
};

View file

@ -3,53 +3,29 @@ use std::time::Instant;
use histogram::Histogram;
use aquatic_common::access_list::AccessListMode;
use crate::common::*;
use crate::config::Config;
pub fn clean_connections_and_torrents(state: &State) {
let now = Instant::now();
{
let mut connections = state.connections.lock();
connections.retain(|_, v| v.0 > now);
connections.shrink_to_fit();
pub fn update_access_list(config: &Config, state: &State) {
match config.access_list.mode {
AccessListMode::White | AccessListMode::Black => {
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
::log::error!("Update access list from path: {:?}", err);
}
}
AccessListMode::Off => {}
}
let mut torrents = state.torrents.lock();
clean_torrent_map(&mut torrents.ipv4, now);
clean_torrent_map(&mut torrents.ipv6, now);
}
#[inline]
fn clean_torrent_map<I: Ip>(torrents: &mut TorrentMap<I>, now: Instant) {
torrents.retain(|_, torrent| {
let num_seeders = &mut torrent.num_seeders;
let num_leechers = &mut torrent.num_leechers;
pub fn clean_connections(state: &State) {
let now = Instant::now();
torrent.peers.retain(|_, peer| {
let keep = peer.valid_until.0 > now;
let mut connections = state.connections.lock();
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
}
keep
});
!torrent.peers.is_empty()
});
torrents.shrink_to_fit();
connections.retain(|_, v| v.0 > now);
connections.shrink_to_fit();
}
pub fn gather_and_print_statistics(state: &State, config: &Config) {

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::convert::TryInto;
use std::io::{self, Cursor, Write};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
@ -37,7 +38,7 @@ pub struct ScrapeResponse {
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ErrorResponse {
pub transaction_id: TransactionId,
pub message: String,
pub message: Cow<'static, str>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
@ -224,7 +225,9 @@ impl Response {
Ok((ErrorResponse {
transaction_id: TransactionId(transaction_id),
message: String::from_utf8_lossy(&inner[position..]).into(),
message: String::from_utf8_lossy(&inner[position..])
.into_owned()
.into(),
})
.into())
}
@ -262,7 +265,7 @@ impl Response {
}
_ => Ok((ErrorResponse {
transaction_id: TransactionId(transaction_id),
message: "Invalid action".to_string(),
message: "Invalid action".into(),
})
.into()),
}

View file

@ -1,6 +1,8 @@
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Instant;
use aquatic_common::access_list::AccessList;
use crossbeam_channel::{Receiver, Sender};
use hashbrown::HashMap;
use indexmap::IndexMap;
@ -12,6 +14,8 @@ pub use aquatic_common::ValidUntil;
use aquatic_ws_protocol::*;
use crate::config::Config;
pub const LISTENER_TOKEN: Token = Token(0);
pub const CHANNEL_TOKEN: Token = Token(1);
@ -84,14 +88,62 @@ pub struct TorrentMaps {
pub ipv6: TorrentMap,
}
impl TorrentMaps {
pub fn clean(&mut self, config: &Config, access_list: &Arc<AccessList>) {
Self::clean_torrent_map(config, access_list, &mut self.ipv4);
Self::clean_torrent_map(config, access_list, &mut self.ipv6);
}
fn clean_torrent_map(
config: &Config,
access_list: &Arc<AccessList>,
torrent_map: &mut TorrentMap,
) {
let now = Instant::now();
torrent_map.retain(|info_hash, torrent_data| {
if !access_list.allows(config.access_list.mode, &info_hash.0) {
return false;
}
let num_seeders = &mut torrent_data.num_seeders;
let num_leechers = &mut torrent_data.num_leechers;
torrent_data.peers.retain(|_, peer| {
let keep = peer.valid_until.0 >= now;
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
}
keep
});
!torrent_data.peers.is_empty()
});
torrent_map.shrink_to_fit();
}
}
#[derive(Clone)]
pub struct State {
pub access_list: Arc<AccessList>,
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
}
impl Default for State {
fn default() -> Self {
Self {
access_list: Arc::new(Default::default()),
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
}
}

View file

@ -1,5 +1,6 @@
use std::net::SocketAddr;
use aquatic_common::access_list::AccessListConfig;
use serde::{Deserialize, Serialize};
use aquatic_cli_helpers::LogLevel;
@ -21,6 +22,7 @@ pub struct Config {
pub cleaning: CleaningConfig,
pub statistics: StatisticsConfig,
pub privileges: PrivilegeConfig,
pub access_list: AccessListConfig,
}
impl aquatic_cli_helpers::Config for Config {
@ -105,6 +107,7 @@ impl Default for Config {
cleaning: CleaningConfig::default(),
statistics: StatisticsConfig::default(),
privileges: PrivilegeConfig::default(),
access_list: AccessListConfig::default(),
}
}
}

View file

@ -24,12 +24,16 @@ pub const APP_NAME: &str = "aquatic_ws: WebTorrent tracker";
pub fn run(config: Config) -> anyhow::Result<()> {
let state = State::default();
tasks::update_access_list(&config, &state);
start_workers(config.clone(), state.clone())?;
loop {
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
tasks::clean_torrents(&state);
tasks::update_access_list(&config, &state);
state.torrent_maps.lock().clean(&config, &state.access_list);
}
}
@ -53,6 +57,7 @@ pub fn start_workers(config: Config, state: State) -> anyhow::Result<()> {
for i in 0..config.socket_workers {
let config = config.clone();
let state = state.clone();
let socket_worker_statuses = socket_worker_statuses.clone();
let in_message_sender = in_message_sender.clone();
let opt_tls_acceptor = opt_tls_acceptor.clone();
@ -69,6 +74,7 @@ pub fn start_workers(config: Config, state: State) -> anyhow::Result<()> {
.spawn(move || {
network::run_socket_worker(
config,
state,
i,
socket_worker_statuses,
poll,

View file

@ -21,10 +21,10 @@ pub enum Stream {
impl Stream {
#[inline]
pub fn get_peer_addr(&self) -> SocketAddr {
pub fn get_peer_addr(&self) -> ::std::io::Result<SocketAddr> {
match self {
Self::TcpStream(stream) => stream.peer_addr().unwrap(),
Self::TlsStream(stream) => stream.get_ref().peer_addr().unwrap(),
Self::TcpStream(stream) => stream.peer_addr(),
Self::TlsStream(stream) => stream.get_ref().peer_addr(),
}
}
@ -145,7 +145,7 @@ impl HandshakeMachine {
);
(Some(Either::Right(Self::TlsStream(stream))), false)
},
}
Err(native_tls::HandshakeError::WouldBlock(handshake)) => {
(Some(Either::Right(Self::TlsMidHandshake(handshake))), true)
}
@ -162,21 +162,26 @@ impl HandshakeMachine {
result: Result<WebSocket<Stream>, HandshakeError<ServerHandshake<Stream, NoCallback>>>,
) -> (Option<Either<EstablishedWs, Self>>, bool) {
match result {
Ok(mut ws) => {
let peer_addr = ws.get_mut().get_peer_addr();
Ok(mut ws) => match ws.get_mut().get_peer_addr() {
Ok(peer_addr) => {
::log::trace!(
"established ws handshake with peer with addr: {:?}",
peer_addr
);
::log::trace!(
"established ws handshake with peer with addr: {:?}",
peer_addr
);
let established_ws = EstablishedWs { ws, peer_addr };
let established_ws = EstablishedWs {
ws,
peer_addr,
};
(Some(Either::Left(established_ws)), false)
}
Err(err) => {
::log::info!(
"get_peer_addr failed during handshake, removing connection: {:?}",
err
);
(Some(Either::Left(established_ws)), false)
}
(None, false)
}
},
Err(HandshakeError::Interrupted(handshake)) => (
Some(Either::Right(HandshakeMachine::WsMidHandshake(handshake))),
true,

View file

@ -1,5 +1,6 @@
use std::io::ErrorKind;
use std::time::Duration;
use std::vec::Drain;
use crossbeam_channel::Receiver;
use hashbrown::HashMap;
@ -23,6 +24,7 @@ use utils::*;
pub fn run_socket_worker(
config: Config,
state: State,
socket_worker_index: usize,
socket_worker_statuses: SocketWorkerStatuses,
poll: Poll,
@ -36,6 +38,7 @@ pub fn run_socket_worker(
run_poll_loop(
config,
&state,
socket_worker_index,
poll,
in_message_sender,
@ -53,6 +56,7 @@ pub fn run_socket_worker(
pub fn run_poll_loop(
config: Config,
state: &State,
socket_worker_index: usize,
mut poll: Poll,
in_message_sender: InMessageSender,
@ -76,6 +80,7 @@ pub fn run_poll_loop(
.unwrap();
let mut connections: ConnectionMap = HashMap::new();
let mut local_responses = Vec::new();
let mut poll_token_counter = Token(0usize);
let mut iter_counter = 0usize;
@ -100,7 +105,10 @@ pub fn run_poll_loop(
);
} else if token != CHANNEL_TOKEN {
run_handshakes_and_read_messages(
&config,
state,
socket_worker_index,
&mut local_responses,
&in_message_sender,
&opt_tls_acceptor,
&mut poll,
@ -110,7 +118,12 @@ pub fn run_poll_loop(
);
}
send_out_messages(&mut poll, &out_message_receiver, &mut connections);
send_out_messages(
&mut poll,
local_responses.drain(..),
&out_message_receiver,
&mut connections,
);
}
// Remove inactive connections, but not every iteration
@ -165,7 +178,10 @@ fn accept_new_streams(
/// On the stream given by poll_token, get TLS (if requested) and tungstenite
/// up and running, then read messages and pass on through channel.
pub fn run_handshakes_and_read_messages(
config: &Config,
state: &State,
socket_worker_index: usize,
local_responses: &mut Vec<(ConnectionMeta, OutMessage)>,
in_message_sender: &InMessageSender,
opt_tls_acceptor: &Option<TlsAcceptor>, // If set, run TLS
poll: &mut Poll,
@ -173,6 +189,8 @@ pub fn run_handshakes_and_read_messages(
poll_token: Token,
valid_until: ValidUntil,
) {
let access_list_mode = config.access_list.mode;
loop {
if let Some(established_ws) = connections
.get_mut(&poll_token)
@ -188,21 +206,50 @@ pub fn run_handshakes_and_read_messages(
match established_ws.ws.read_message() {
Ok(ws_message) => {
if let Ok(in_message) = InMessage::from_ws_message(ws_message) {
let naive_peer_addr = established_ws.peer_addr;
let converted_peer_ip = convert_ipv4_mapped_ipv6(naive_peer_addr.ip());
let naive_peer_addr = established_ws.peer_addr;
let converted_peer_ip = convert_ipv4_mapped_ipv6(naive_peer_addr.ip());
let meta = ConnectionMeta {
worker_index: socket_worker_index,
poll_token,
naive_peer_addr,
converted_peer_ip,
};
let meta = ConnectionMeta {
worker_index: socket_worker_index,
poll_token,
naive_peer_addr,
converted_peer_ip,
};
debug!("read message");
debug!("read message");
if let Err(err) = in_message_sender.send((meta, in_message)) {
error!("InMessageSender: couldn't send message: {:?}", err);
match InMessage::from_ws_message(ws_message) {
Ok(InMessage::AnnounceRequest(ref request))
if !state
.access_list
.allows(access_list_mode, &request.info_hash.0) =>
{
let out_message = OutMessage::ErrorResponse(ErrorResponse {
failure_reason: "Info hash not allowed".into(),
action: Some(ErrorResponseAction::Announce),
info_hash: Some(request.info_hash),
});
local_responses.push((meta, out_message));
}
Ok(in_message) => {
if let Err(err) = in_message_sender.send((meta, in_message)) {
error!("InMessageSender: couldn't send message: {:?}", err);
}
}
Err(_) => {
// FIXME: maybe this condition just occurs when enough data hasn't been recevied?
/*
info!("error parsing message: {:?}", err);
let out_message = OutMessage::ErrorResponse(ErrorResponse {
failure_reason: "Error parsing message".into(),
action: None,
info_hash: None,
});
local_responses.push((meta, out_message));
*/
}
}
}
@ -242,12 +289,13 @@ pub fn run_handshakes_and_read_messages(
/// Read messages from channel, send to peers
pub fn send_out_messages(
poll: &mut Poll,
local_responses: Drain<(ConnectionMeta, OutMessage)>,
out_message_receiver: &Receiver<(ConnectionMeta, OutMessage)>,
connections: &mut ConnectionMap,
) {
let len = out_message_receiver.len();
for (meta, out_message) in out_message_receiver.try_iter().take(len) {
for (meta, out_message) in local_responses.chain(out_message_receiver.try_iter().take(len)) {
let opt_established_ws = connections
.get_mut(&meta.poll_token)
.and_then(Connection::get_established_ws);

View file

@ -1,45 +1,18 @@
use std::time::Instant;
use aquatic_common::access_list::AccessListMode;
use histogram::Histogram;
use crate::common::*;
use crate::config::Config;
pub fn clean_torrents(state: &State) {
fn clean_torrent_map(torrent_map: &mut TorrentMap) {
let now = Instant::now();
torrent_map.retain(|_, torrent_data| {
let num_seeders = &mut torrent_data.num_seeders;
let num_leechers = &mut torrent_data.num_leechers;
torrent_data.peers.retain(|_, peer| {
let keep = peer.valid_until.0 >= now;
if !keep {
match peer.status {
PeerStatus::Seeding => {
*num_seeders -= 1;
}
PeerStatus::Leeching => {
*num_leechers -= 1;
}
_ => (),
};
}
keep
});
!torrent_data.peers.is_empty()
});
torrent_map.shrink_to_fit();
pub fn update_access_list(config: &Config, state: &State) {
match config.access_list.mode {
AccessListMode::White | AccessListMode::Black => {
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
::log::error!("Couldn't update access list: {:?}", err);
}
}
AccessListMode::Off => {}
}
let mut torrent_maps = state.torrent_maps.lock();
clean_torrent_map(&mut torrent_maps.ipv4);
clean_torrent_map(&mut torrent_maps.ipv6);
}
pub fn print_statistics(state: &State) {

View file

@ -12,6 +12,7 @@ pub struct Statistics {
pub responses_offer: AtomicUsize,
pub responses_answer: AtomicUsize,
pub responses_scrape: AtomicUsize,
pub responses_error: AtomicUsize,
}
#[derive(Clone)]

View file

@ -85,13 +85,16 @@ fn monitor_statistics(state: LoadTestState, config: &Config) {
statistics.responses_answer.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
let responses_scrape_per_second =
statistics.responses_scrape.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
let responses_error_per_second =
statistics.responses_error.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
let responses_announce_per_second = responses_announce / interval_f64;
let responses_per_second = responses_announce_per_second
+ responses_offer_per_second
+ responses_answer_per_second
+ responses_scrape_per_second;
+ responses_scrape_per_second
+ responses_error_per_second;
report_avg_response_vec.push(responses_per_second);
@ -105,6 +108,7 @@ fn monitor_statistics(state: LoadTestState, config: &Config) {
println!(" - Offer responses: {:.2}", responses_offer_per_second);
println!(" - Answer responses: {:.2}", responses_answer_per_second);
println!(" - Scrape responses: {:.2}", responses_scrape_per_second);
println!(" - Error responses: {:.2}", responses_error_per_second);
let time_elapsed = start_time.elapsed();
let duration = Duration::from_secs(config.duration as u64);

View file

@ -151,6 +151,16 @@ impl Connection {
self.can_send = true;
}
Ok(OutMessage::ErrorResponse(response)) => {
state
.statistics
.responses_error
.fetch_add(1, Ordering::SeqCst);
eprintln!("received error response: {:?}", response.failure_reason);
self.can_send = true;
}
Err(err) => {
eprintln!("error deserializing offer: {:?}", err);
}

View file

@ -1,6 +1,81 @@
use serde::{de::Visitor, Deserializer, Serializer};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use super::{AnnounceAction, ScrapeAction};
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PeerId(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct InfoHash(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OfferId(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
/// Some kind of nested structure from https://www.npmjs.com/package/simple-peer
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct JsonValue(pub ::serde_json::Value);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnnounceAction;
impl Serialize for AnnounceAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("announce")
}
}
impl<'de> Deserialize<'de> for AnnounceAction {
fn deserialize<D>(deserializer: D) -> Result<AnnounceAction, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(AnnounceActionVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrapeAction;
impl Serialize for ScrapeAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("scrape")
}
}
impl<'de> Deserialize<'de> for ScrapeAction {
fn deserialize<D>(deserializer: D) -> Result<ScrapeAction, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ScrapeActionVisitor)
}
}
pub struct AnnounceActionVisitor;
@ -44,7 +119,7 @@ impl<'de> Visitor<'de> for ScrapeActionVisitor {
}
}
pub fn serialize_20_bytes<S>(data: &[u8; 20], serializer: S) -> Result<S::Ok, S::Error>
fn serialize_20_bytes<S>(data: &[u8; 20], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@ -102,7 +177,7 @@ impl<'de> Visitor<'de> for TwentyByteVisitor {
}
#[inline]
pub fn deserialize_20_bytes<'de, D>(deserializer: D) -> Result<[u8; 20], D::Error>
fn deserialize_20_bytes<'de, D>(deserializer: D) -> Result<[u8; 20], D::Error>
where
D: Deserializer<'de>,
{
@ -113,7 +188,7 @@ where
mod tests {
use quickcheck_macros::quickcheck;
use crate::InfoHash;
use crate::common::InfoHash;
fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash {
let mut arr = [0u8; 20];

View file

@ -1,281 +1,22 @@
use anyhow::Context;
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
//! WebTorrent protocol implementation
//!
//! Typical announce workflow:
//! - Peer A sends announce request with info hash and offers
//! - Tracker sends on offers to other peers announcing with that info hash and
//! sends back announce response to peer A
//! - Tracker receives answers to those offers from other peers and send them
//! on to peer A
//!
//! Typical scrape workflow
//! - Peer sends scrape request and receives scrape response
mod serde_helpers;
pub mod common;
pub mod request;
pub mod response;
use serde_helpers::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnnounceAction;
impl Serialize for AnnounceAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("announce")
}
}
impl<'de> Deserialize<'de> for AnnounceAction {
fn deserialize<D>(deserializer: D) -> Result<AnnounceAction, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(AnnounceActionVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrapeAction;
impl Serialize for ScrapeAction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str("scrape")
}
}
impl<'de> Deserialize<'de> for ScrapeAction {
fn deserialize<D>(deserializer: D) -> Result<ScrapeAction, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ScrapeActionVisitor)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PeerId(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct InfoHash(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OfferId(
#[serde(
deserialize_with = "deserialize_20_bytes",
serialize_with = "serialize_20_bytes"
)]
pub [u8; 20],
);
/// Some kind of nested structure from https://www.npmjs.com/package/simple-peer
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct JsonValue(pub ::serde_json::Value);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnnounceEvent {
Started,
Stopped,
Completed,
Update,
}
impl Default for AnnounceEvent {
fn default() -> Self {
Self::Update
}
}
/// Apparently, these are sent to a number of peers when they are set
/// in an AnnounceRequest
/// action = "announce"
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlemanOfferToPeer {
pub action: AnnounceAction,
/// Peer id of peer sending offer
/// Note: if equal to client peer_id, client ignores offer
pub peer_id: PeerId,
pub info_hash: InfoHash,
/// Gets copied from AnnounceRequestOffer
pub offer: JsonValue,
/// Gets copied from AnnounceRequestOffer
pub offer_id: OfferId,
}
/// If announce request has answer = true, send this to peer with
/// peer id == "to_peer_id" field
/// Action field should be 'announce'
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlemanAnswerToPeer {
pub action: AnnounceAction,
/// Note: if equal to client peer_id, client ignores answer
pub peer_id: PeerId,
pub info_hash: InfoHash,
pub answer: JsonValue,
pub offer_id: OfferId,
}
/// Element of AnnounceRequest.offers
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceRequestOffer {
pub offer: JsonValue,
pub offer_id: OfferId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceRequest {
pub action: AnnounceAction,
pub info_hash: InfoHash,
pub peer_id: PeerId,
/// Just called "left" in protocol. Is set to None in some cases, such as
/// when opening a magnet link
#[serde(rename = "left")]
pub bytes_left: Option<usize>,
/// Can be empty. Then, default is "update"
#[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<AnnounceEvent>,
/// Only when this is an array offers are sent to random peers
/// Length of this is number of peers wanted?
/// Max length of this is 10 in reference client code
/// Not sent when announce event is stopped or completed
pub offers: Option<Vec<AnnounceRequestOffer>>,
/// Seems to only get sent by client when sending offers, and is also same
/// as length of offers vector (or at least never less)
/// Max length of this is 10 in reference client code
/// Could probably be ignored, `offers.len()` should provide needed info
pub numwant: Option<usize>,
/// If empty, send response before sending offers (or possibly "skip sending update back"?)
/// Else, send MiddlemanAnswerToPeer to peer with "to_peer_id" as peer_id.
/// I think using Option is good, it seems like this isn't always set
/// (same as `offers`)
pub answer: Option<JsonValue>,
/// Likely undefined if !(answer == true)
pub to_peer_id: Option<PeerId>,
/// Sent if answer is set
pub offer_id: Option<OfferId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceResponse {
pub action: AnnounceAction,
pub info_hash: InfoHash,
/// Client checks if this is null, not clear why
pub complete: usize,
pub incomplete: usize,
#[serde(rename = "interval")]
pub announce_interval: usize, // Default 2 min probably
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ScrapeRequestInfoHashes {
Single(InfoHash),
Multiple(Vec<InfoHash>),
}
impl ScrapeRequestInfoHashes {
pub fn as_vec(self) -> Vec<InfoHash> {
match self {
Self::Single(info_hash) => vec![info_hash],
Self::Multiple(info_hashes) => info_hashes,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeRequest {
pub action: ScrapeAction,
// If omitted, scrape for all torrents, apparently
// There is some kind of parsing here too which accepts a single info hash
// and puts it into a vector
#[serde(rename = "info_hash")]
pub info_hashes: Option<ScrapeRequestInfoHashes>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeStatistics {
pub complete: usize,
pub incomplete: usize,
pub downloaded: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeResponse {
pub action: ScrapeAction,
pub files: HashMap<InfoHash, ScrapeStatistics>,
// Looks like `flags` field is ignored in reference client
// pub flags: HashMap<String, usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InMessage {
AnnounceRequest(AnnounceRequest),
ScrapeRequest(ScrapeRequest),
}
impl InMessage {
#[inline]
pub fn to_ws_message(&self) -> ::tungstenite::Message {
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
}
#[inline]
pub fn from_ws_message(ws_message: tungstenite::Message) -> ::anyhow::Result<Self> {
use tungstenite::Message::Text;
let mut text = if let Text(text) = ws_message {
text
} else {
return Err(anyhow::anyhow!("Message is not text"));
};
return ::simd_json::serde::from_str(&mut text).context("deserialize with serde");
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OutMessage {
Offer(MiddlemanOfferToPeer),
Answer(MiddlemanAnswerToPeer),
AnnounceResponse(AnnounceResponse),
ScrapeResponse(ScrapeResponse),
}
impl OutMessage {
#[inline]
pub fn to_ws_message(&self) -> tungstenite::Message {
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
}
#[inline]
pub fn from_ws_message(message: ::tungstenite::Message) -> ::anyhow::Result<Self> {
use tungstenite::Message::{Binary, Text};
let mut text = match message {
Text(text) => text,
Binary(bytes) => String::from_utf8(bytes)?,
_ => return Err(anyhow::anyhow!("Message is neither text nor bytes")),
};
Ok(::simd_json::serde::from_str(&mut text)?)
}
}
pub use common::*;
pub use request::*;
pub use response::*;
#[cfg(test)]
mod tests {

View file

@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use crate::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnnounceEvent {
Started,
Stopped,
Completed,
Update,
}
impl Default for AnnounceEvent {
fn default() -> Self {
Self::Update
}
}
/// Element of AnnounceRequest.offers
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceRequestOffer {
pub offer: JsonValue,
pub offer_id: OfferId,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceRequest {
pub action: AnnounceAction,
pub info_hash: InfoHash,
pub peer_id: PeerId,
/// Just called "left" in protocol. Is set to None in some cases, such as
/// when opening a magnet link
#[serde(rename = "left")]
pub bytes_left: Option<usize>,
/// Can be empty. Then, default is "update"
#[serde(skip_serializing_if = "Option::is_none")]
pub event: Option<AnnounceEvent>,
/// Only when this is an array offers are sent to random peers
/// Length of this is number of peers wanted?
/// Max length of this is 10 in reference client code
/// Not sent when announce event is stopped or completed
pub offers: Option<Vec<AnnounceRequestOffer>>,
/// Seems to only get sent by client when sending offers, and is also same
/// as length of offers vector (or at least never less)
/// Max length of this is 10 in reference client code
/// Could probably be ignored, `offers.len()` should provide needed info
pub numwant: Option<usize>,
/// If empty, send response before sending offers (or possibly "skip sending update back"?)
/// Else, send MiddlemanAnswerToPeer to peer with "to_peer_id" as peer_id.
/// I think using Option is good, it seems like this isn't always set
/// (same as `offers`)
pub answer: Option<JsonValue>,
/// Likely undefined if !(answer == true)
pub to_peer_id: Option<PeerId>,
/// Sent if answer is set
pub offer_id: Option<OfferId>,
}

View file

@ -0,0 +1,36 @@
use anyhow::Context;
use serde::{Deserialize, Serialize};
pub mod announce;
pub mod scrape;
pub use announce::*;
pub use scrape::*;
/// Message received by tracker
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum InMessage {
AnnounceRequest(AnnounceRequest),
ScrapeRequest(ScrapeRequest),
}
impl InMessage {
#[inline]
pub fn to_ws_message(&self) -> ::tungstenite::Message {
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
}
#[inline]
pub fn from_ws_message(ws_message: tungstenite::Message) -> ::anyhow::Result<Self> {
use tungstenite::Message::Text;
let mut text = if let Text(text) = ws_message {
text
} else {
return Err(anyhow::anyhow!("Message is not text"));
};
return ::simd_json::serde::from_str(&mut text).context("deserialize with serde");
}
}

View file

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
use crate::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ScrapeRequestInfoHashes {
Single(InfoHash),
Multiple(Vec<InfoHash>),
}
impl ScrapeRequestInfoHashes {
pub fn as_vec(self) -> Vec<InfoHash> {
match self {
Self::Single(info_hash) => vec![info_hash],
Self::Multiple(info_hashes) => info_hashes,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeRequest {
pub action: ScrapeAction,
// If omitted, scrape for all torrents, apparently
// There is some kind of parsing here too which accepts a single info hash
// and puts it into a vector
#[serde(rename = "info_hash")]
pub info_hashes: Option<ScrapeRequestInfoHashes>,
}

View file

@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
use crate::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceResponse {
pub action: AnnounceAction,
pub info_hash: InfoHash,
/// Client checks if this is null, not clear why
pub complete: usize,
pub incomplete: usize,
#[serde(rename = "interval")]
pub announce_interval: usize, // Default 2 min probably
}

View file

@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
use crate::common::*;
/// If announce request has answer = true, send this to peer with
/// peer id == "to_peer_id" field
/// Action field should be 'announce'
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlemanAnswerToPeer {
pub action: AnnounceAction,
/// Note: if equal to client peer_id, client ignores answer
pub peer_id: PeerId,
pub info_hash: InfoHash,
pub answer: JsonValue,
pub offer_id: OfferId,
}

View file

@ -0,0 +1,24 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ErrorResponseAction {
Announce,
Scrape,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorResponse {
#[serde(rename = "failure reason")]
pub failure_reason: Cow<'static, str>,
/// Action of original request
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<ErrorResponseAction>,
// Should not be renamed
#[serde(skip_serializing_if = "Option::is_none")]
pub info_hash: Option<InfoHash>,
}

View file

@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
pub mod announce;
pub mod answer;
pub mod error;
pub mod offer;
pub mod scrape;
pub use announce::*;
pub use answer::*;
pub use error::*;
pub use offer::*;
pub use scrape::*;
/// Message sent by tracker
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OutMessage {
Offer(MiddlemanOfferToPeer),
Answer(MiddlemanAnswerToPeer),
AnnounceResponse(AnnounceResponse),
ScrapeResponse(ScrapeResponse),
ErrorResponse(ErrorResponse),
}
impl OutMessage {
#[inline]
pub fn to_ws_message(&self) -> tungstenite::Message {
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
}
#[inline]
pub fn from_ws_message(message: ::tungstenite::Message) -> ::anyhow::Result<Self> {
use tungstenite::Message::{Binary, Text};
let mut text = match message {
Text(text) => text,
Binary(bytes) => String::from_utf8(bytes)?,
_ => return Err(anyhow::anyhow!("Message is neither text nor bytes")),
};
Ok(::simd_json::serde::from_str(&mut text)?)
}
}

View file

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use crate::common::*;
/// Apparently, these are sent to a number of peers when they are set
/// in an AnnounceRequest
/// action = "announce"
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MiddlemanOfferToPeer {
pub action: AnnounceAction,
/// Peer id of peer sending offer
/// Note: if equal to client peer_id, client ignores offer
pub peer_id: PeerId,
pub info_hash: InfoHash,
/// Gets copied from AnnounceRequestOffer
pub offer: JsonValue,
/// Gets copied from AnnounceRequestOffer
pub offer_id: OfferId,
}

View file

@ -0,0 +1,19 @@
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use crate::common::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeResponse {
pub action: ScrapeAction,
pub files: HashMap<InfoHash, ScrapeStatistics>,
// Looks like `flags` field is ignored in reference client
// pub flags: HashMap<String, usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrapeStatistics {
pub complete: usize,
pub incomplete: usize,
pub downloaded: usize,
}