mirror of
https://github.com/YGGverse/aquatic.git
synced 2026-04-02 02:35:31 +00:00
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:
commit
d0b8ed3f64
43 changed files with 1444 additions and 972 deletions
178
Cargo.lock
generated
178
Cargo.lock
generated
|
|
@ -1,5 +1,7 @@
|
||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
@ -23,9 +25,9 @@ checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.7.4"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
|
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -43,9 +45,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.43"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
|
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aquatic"
|
name = "aquatic"
|
||||||
|
|
@ -72,8 +74,13 @@ dependencies = [
|
||||||
name = "aquatic_common"
|
name = "aquatic_common"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"arc-swap",
|
||||||
|
"hashbrown 0.11.2",
|
||||||
|
"hex",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rand",
|
"rand",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -275,6 +282,12 @@ dependencies = [
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -350,9 +363,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "0.2.16"
|
version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
|
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -362,9 +375,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.7.0"
|
version = "3.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
|
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
|
@ -389,9 +402,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.70"
|
version = "1.0.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
|
|
@ -425,22 +438,22 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.14.1"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45"
|
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
"libc",
|
||||||
|
"once_cell",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
|
checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -448,9 +461,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
|
|
@ -685,9 +698,9 @@ checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "1.7.1"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3"
|
checksum = "ac5956d4e63858efaec57e0d6c1c2f6a41e1487f830314a324ccd7e2223a7ca0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "halfbrown"
|
name = "halfbrown"
|
||||||
|
|
@ -715,7 +728,7 @@ version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.7.4",
|
"ahash 0.7.6",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -742,9 +755,9 @@ checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
|
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
|
|
@ -792,9 +805,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
|
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
@ -816,9 +829,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.53"
|
version = "0.3.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d"
|
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
@ -831,9 +844,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.101"
|
version = "0.2.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
|
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
|
|
@ -950,14 +963,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.19.1"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2"
|
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1070,9 +1084,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.66"
|
version = "0.9.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
|
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
|
|
@ -1114,9 +1128,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.19"
|
version = "0.3.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plotters"
|
name = "plotters"
|
||||||
|
|
@ -1148,15 +1162,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.10"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "privdrop"
|
name = "privdrop"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebd4c2739642e70439d1c0d9545beec45c1e54128739b3cda29bf2c366028c87"
|
checksum = "4c02cf257b10e4b807bccadb19630d5dea7e0369c3c5e84673ee8e58dc8da6a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"nix",
|
"nix",
|
||||||
|
|
@ -1164,9 +1178,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.29"
|
version = "1.0.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
@ -1195,9 +1209,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.9"
|
version = "1.0.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
@ -1235,9 +1249,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_distr"
|
name = "rand_distr"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "051b398806e42b9cd04ad9ec8f81e355d0a382c543ac6672c62f5a5b452ef142"
|
checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
@ -1444,9 +1458,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.67"
|
version = "1.0.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
|
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
|
@ -1468,9 +1482,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-json"
|
name = "simd-json"
|
||||||
version = "0.4.7"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "529edb21cdc2629d7214de58672ec9fe5678b623e8fffb03327f77d7291d8865"
|
checksum = "e67eb096671712144fb1357787c4312720c99444f52900ca2a20bee57a02cc64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"halfbrown",
|
"halfbrown",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1487,9 +1501,9 @@ checksum = "c970da16e7c682fa90a261cf0724dee241c9f7831635ecc4e988ae8f3b505559"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simplelog"
|
name = "simplelog"
|
||||||
version = "0.10.0"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0"
|
checksum = "85d04ae642154220ef00ee82c36fb07853c10a4f2a0ca6719f9991211d2eb959"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1498,15 +1512,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.4"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
|
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.6.1"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smartstring"
|
name = "smartstring"
|
||||||
|
|
@ -1519,9 +1533,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
|
checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|
@ -1535,9 +1549,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.76"
|
version = "1.0.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1546,9 +1560,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.12.5"
|
version = "0.12.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa"
|
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1600,18 +1614,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.29"
|
version = "1.0.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
|
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.29"
|
version = "1.0.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
|
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1640,9 +1654,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.3.1"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
|
checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
|
|
@ -1689,9 +1703,9 @@ checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.6"
|
version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
|
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
|
|
@ -1704,9 +1718,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
|
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
|
@ -1781,9 +1795,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.76"
|
version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
|
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
|
|
@ -1791,9 +1805,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.76"
|
version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041"
|
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|
@ -1806,9 +1820,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.76"
|
version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef"
|
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
|
@ -1816,9 +1830,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.76"
|
version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad"
|
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1829,15 +1843,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.76"
|
version = "0.2.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
|
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.53"
|
version = "0.3.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c"
|
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -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
|
`address` under the `network` section (listening address). This goes for all
|
||||||
three protocols.
|
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`.
|
`src/lib/config.rs` in crates `aquatic_udp`, `aquatic_http`, `aquatic_ws`.
|
||||||
|
|
||||||
## Details on implementations
|
## Details on implementations
|
||||||
|
|
|
||||||
25
TODO.md
25
TODO.md
|
|
@ -1,13 +1,23 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
* Consider turning on safety and override flags in mimalloc (mostly for
|
* access lists:
|
||||||
simd-json)
|
* 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
|
## General
|
||||||
* extract response peers: extract "one extra" to compensate for removal,
|
* extract response peers: extract "one extra" to compensate for removal,
|
||||||
of sender if present in selection? maybe make criterion benchmark,
|
of sender if present in selection? maybe make criterion benchmark,
|
||||||
optimize. consider rerunning udp benchmark, last change (filtering out
|
optimize
|
||||||
sender) might have had an adverse impact on performance.
|
|
||||||
|
|
||||||
## aquatic_http_load_test
|
## aquatic_http_load_test
|
||||||
* how handle large number of peers for "popular" torrents in keepalive mode?
|
* how handle large number of peers for "popular" torrents in keepalive mode?
|
||||||
|
|
@ -24,7 +34,6 @@
|
||||||
|
|
||||||
## aquatic_http
|
## aquatic_http
|
||||||
* test torrent transfer with real clients
|
* test torrent transfer with real clients
|
||||||
* test tls
|
|
||||||
* scrape: does it work (serialization etc), and with multiple hashes?
|
* scrape: does it work (serialization etc), and with multiple hashes?
|
||||||
* 'left' optional in magnet requests? Probably not. Transmission sends huge
|
* 'left' optional in magnet requests? Probably not. Transmission sends huge
|
||||||
positive number.
|
positive number.
|
||||||
|
|
@ -40,12 +49,6 @@
|
||||||
scrape requests I suppose.
|
scrape requests I suppose.
|
||||||
|
|
||||||
## aquatic_ws
|
## 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,
|
* websocket_max_frame_size should be at least something like 64 * 1024,
|
||||||
maybe put it and message size at 128k just to be sure
|
maybe put it and message size at 128k just to be sure
|
||||||
* test transfer, specifically ipv6/ipv4 mapping
|
* test transfer, specifically ipv6/ipv4 mapping
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,10 @@ repository = "https://github.com/greatest-ape/aquatic"
|
||||||
name = "aquatic_common"
|
name = "aquatic_common"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
arc-swap = "1"
|
||||||
|
hashbrown = "0.11.2"
|
||||||
|
hex = "0.4"
|
||||||
indexmap = "1"
|
indexmap = "1"
|
||||||
rand = { version = "0.8", features = ["small_rng"] }
|
rand = { version = "0.8", features = ["small_rng"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
||||||
93
aquatic_common/src/access_list.rs
Normal file
93
aquatic_common/src/access_list.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ use std::time::{Duration, Instant};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub mod access_list;
|
||||||
|
|
||||||
/// Peer or connection valid until this instant
|
/// Peer or connection valid until this instant
|
||||||
///
|
///
|
||||||
/// Used instead of "last seen" or similar to hopefully prevent arithmetic
|
/// Used instead of "last seen" or similar to hopefully prevent arithmetic
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessList;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
|
|
@ -16,6 +18,8 @@ use aquatic_http_protocol::common::*;
|
||||||
use aquatic_http_protocol::request::Request;
|
use aquatic_http_protocol::request::Request;
|
||||||
use aquatic_http_protocol::response::{Response, ResponsePeer};
|
use aquatic_http_protocol::response::{Response, ResponsePeer};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
pub const LISTENER_TOKEN: Token = Token(0);
|
pub const LISTENER_TOKEN: Token = Token(0);
|
||||||
pub const CHANNEL_TOKEN: Token = Token(1);
|
pub const CHANNEL_TOKEN: Token = Token(1);
|
||||||
|
|
||||||
|
|
@ -113,14 +117,62 @@ pub struct TorrentMaps {
|
||||||
pub ipv6: TorrentMap<Ipv6Addr>,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
pub access_list: Arc<AccessList>,
|
||||||
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
|
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
access_list: Arc::new(Default::default()),
|
||||||
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
|
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessListConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use aquatic_cli_helpers::LogLevel;
|
use aquatic_cli_helpers::LogLevel;
|
||||||
|
|
@ -21,6 +22,7 @@ pub struct Config {
|
||||||
pub cleaning: CleaningConfig,
|
pub cleaning: CleaningConfig,
|
||||||
pub statistics: StatisticsConfig,
|
pub statistics: StatisticsConfig,
|
||||||
pub privileges: PrivilegeConfig,
|
pub privileges: PrivilegeConfig,
|
||||||
|
pub access_list: AccessListConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl aquatic_cli_helpers::Config for Config {
|
impl aquatic_cli_helpers::Config for Config {
|
||||||
|
|
@ -111,6 +113,7 @@ impl Default for Config {
|
||||||
cleaning: CleaningConfig::default(),
|
cleaning: CleaningConfig::default(),
|
||||||
statistics: StatisticsConfig::default(),
|
statistics: StatisticsConfig::default(),
|
||||||
privileges: PrivilegeConfig::default(),
|
privileges: PrivilegeConfig::default(),
|
||||||
|
access_list: AccessListConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ pub const APP_NAME: &str = "aquatic_http: HTTP/TLS BitTorrent tracker";
|
||||||
pub fn run(config: Config) -> anyhow::Result<()> {
|
pub fn run(config: Config) -> anyhow::Result<()> {
|
||||||
let state = State::default();
|
let state = State::default();
|
||||||
|
|
||||||
|
tasks::update_access_list(&config, &state);
|
||||||
|
|
||||||
start_workers(config.clone(), state.clone())?;
|
start_workers(config.clone(), state.clone())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
|
::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 {
|
for i in 0..config.socket_workers {
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
let state = state.clone();
|
||||||
let socket_worker_statuses = socket_worker_statuses.clone();
|
let socket_worker_statuses = socket_worker_statuses.clone();
|
||||||
let request_channel_sender = request_channel_sender.clone();
|
let request_channel_sender = request_channel_sender.clone();
|
||||||
let opt_tls_acceptor = opt_tls_acceptor.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 || {
|
.spawn(move || {
|
||||||
network::run_socket_worker(
|
network::run_socket_worker(
|
||||||
config,
|
config,
|
||||||
|
state,
|
||||||
i,
|
i,
|
||||||
socket_worker_statuses,
|
socket_worker_statuses,
|
||||||
request_channel_sender,
|
request_channel_sender,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::vec::Drain;
|
use std::vec::Drain;
|
||||||
|
|
||||||
|
use aquatic_http_protocol::request::Request;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use mio::net::TcpListener;
|
use mio::net::TcpListener;
|
||||||
|
|
@ -25,6 +26,7 @@ const CONNECTION_CLEAN_INTERVAL: usize = 2 ^ 22;
|
||||||
|
|
||||||
pub fn run_socket_worker(
|
pub fn run_socket_worker(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
state: State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
socket_worker_statuses: SocketWorkerStatuses,
|
socket_worker_statuses: SocketWorkerStatuses,
|
||||||
request_channel_sender: RequestChannelSender,
|
request_channel_sender: RequestChannelSender,
|
||||||
|
|
@ -38,6 +40,7 @@ pub fn run_socket_worker(
|
||||||
|
|
||||||
run_poll_loop(
|
run_poll_loop(
|
||||||
config,
|
config,
|
||||||
|
&state,
|
||||||
socket_worker_index,
|
socket_worker_index,
|
||||||
request_channel_sender,
|
request_channel_sender,
|
||||||
response_channel_receiver,
|
response_channel_receiver,
|
||||||
|
|
@ -55,6 +58,7 @@ pub fn run_socket_worker(
|
||||||
|
|
||||||
pub fn run_poll_loop(
|
pub fn run_poll_loop(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
state: &State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
request_channel_sender: RequestChannelSender,
|
request_channel_sender: RequestChannelSender,
|
||||||
response_channel_receiver: ResponseChannelReceiver,
|
response_channel_receiver: ResponseChannelReceiver,
|
||||||
|
|
@ -100,6 +104,7 @@ pub fn run_poll_loop(
|
||||||
} else if token != CHANNEL_TOKEN {
|
} else if token != CHANNEL_TOKEN {
|
||||||
handle_connection_read_event(
|
handle_connection_read_event(
|
||||||
&config,
|
&config,
|
||||||
|
&state,
|
||||||
socket_worker_index,
|
socket_worker_index,
|
||||||
&mut poll,
|
&mut poll,
|
||||||
&request_channel_sender,
|
&request_channel_sender,
|
||||||
|
|
@ -179,6 +184,7 @@ fn accept_new_streams(
|
||||||
/// then read requests and pass on through channel.
|
/// then read requests and pass on through channel.
|
||||||
pub fn handle_connection_read_event(
|
pub fn handle_connection_read_event(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
state: &State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
poll: &mut Poll,
|
poll: &mut Poll,
|
||||||
request_channel_sender: &RequestChannelSender,
|
request_channel_sender: &RequestChannelSender,
|
||||||
|
|
@ -187,6 +193,7 @@ pub fn handle_connection_read_event(
|
||||||
poll_token: Token,
|
poll_token: Token,
|
||||||
) {
|
) {
|
||||||
let valid_until = ValidUntil::new(config.cleaning.max_connection_age);
|
let valid_until = ValidUntil::new(config.cleaning.max_connection_age);
|
||||||
|
let access_list_mode = config.access_list.mode;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Get connection, updating valid_until
|
// Get connection, updating valid_until
|
||||||
|
|
@ -203,6 +210,22 @@ pub fn handle_connection_read_event(
|
||||||
|
|
||||||
if let Some(established) = connection.get_established() {
|
if let Some(established) = connection.get_established() {
|
||||||
match established.read_request() {
|
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) => {
|
Ok(request) => {
|
||||||
let meta = ConnectionMeta {
|
let meta = ConnectionMeta {
|
||||||
worker_index: socket_worker_index,
|
worker_index: socket_worker_index,
|
||||||
|
|
@ -210,7 +233,7 @@ pub fn handle_connection_read_event(
|
||||||
peer_addr: established.peer_addr,
|
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)) {
|
if let Err(err) = request_channel_sender.send((meta, request)) {
|
||||||
error!("RequestChannelSender: couldn't send message: {:?}", err);
|
error!("RequestChannelSender: couldn't send message: {:?}", err);
|
||||||
|
|
@ -233,9 +256,7 @@ pub fn handle_connection_read_event(
|
||||||
peer_addr: established.peer_addr,
|
peer_addr: established.peer_addr,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = FailureResponse {
|
let response = FailureResponse::new("Invalid request");
|
||||||
failure_reason: "invalid request".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
local_responses.push((meta, Response::Failure(response)));
|
local_responses.push((meta, Response::Failure(response)));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,18 @@
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use histogram::Histogram;
|
use histogram::Histogram;
|
||||||
|
|
||||||
use crate::common::*;
|
use aquatic_common::access_list::AccessListMode;
|
||||||
|
|
||||||
pub fn clean_torrents(state: &State) {
|
use crate::{common::*, config::Config};
|
||||||
let mut torrent_maps = state.torrent_maps.lock();
|
|
||||||
|
|
||||||
clean_torrent_map(&mut torrent_maps.ipv4);
|
pub fn update_access_list(config: &Config, state: &State) {
|
||||||
clean_torrent_map(&mut torrent_maps.ipv6);
|
match config.access_list.mode {
|
||||||
}
|
AccessListMode::White | AccessListMode::Black => {
|
||||||
|
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
|
||||||
fn clean_torrent_map<I: Ip>(torrent_map: &mut TorrentMap<I>) {
|
::log::error!("Couldn't update access list: {:?}", err);
|
||||||
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
|
AccessListMode::Off => {}
|
||||||
});
|
}
|
||||||
|
|
||||||
!torrent_data.peers.is_empty()
|
|
||||||
});
|
|
||||||
|
|
||||||
torrent_map.shrink_to_fit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_statistics(state: &State) {
|
pub fn print_statistics(state: &State) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
|
|
@ -131,10 +132,16 @@ impl ScrapeResponse {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FailureResponse {
|
pub struct FailureResponse {
|
||||||
#[serde(rename = "failure reason")]
|
#[serde(rename = "failure reason")]
|
||||||
pub failure_reason: String,
|
pub failure_reason: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FailureResponse {
|
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> {
|
fn write<W: Write>(&self, output: &mut W) -> ::std::io::Result<usize> {
|
||||||
let mut bytes_written = 0usize;
|
let mut bytes_written = 0usize;
|
||||||
|
|
||||||
|
|
@ -242,7 +249,7 @@ impl quickcheck::Arbitrary for ScrapeResponse {
|
||||||
impl quickcheck::Arbitrary for FailureResponse {
|
impl quickcheck::Arbitrary for FailureResponse {
|
||||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||||
Self {
|
Self {
|
||||||
failure_reason: String::arbitrary(g),
|
failure_reason: String::arbitrary(g).into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::{atomic::AtomicUsize, Arc};
|
use std::sync::{atomic::AtomicUsize, Arc};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
pub use aquatic_common::ValidUntil;
|
pub use aquatic_common::{access_list::AccessList, ValidUntil};
|
||||||
pub use aquatic_udp_protocol::*;
|
pub use aquatic_udp_protocol::*;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
pub const MAX_PACKET_SIZE: usize = 4096;
|
pub const MAX_PACKET_SIZE: usize = 4096;
|
||||||
|
|
||||||
pub trait Ip: Hash + PartialEq + Eq + Clone + Copy {
|
pub trait Ip: Hash + PartialEq + Eq + Clone + Copy {
|
||||||
|
|
@ -33,6 +36,15 @@ pub struct ConnectionKey {
|
||||||
pub socket_addr: SocketAddr,
|
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>;
|
pub type ConnectionMap = HashMap<ConnectionKey, ValidUntil>;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||||
|
|
@ -108,6 +120,54 @@ pub struct TorrentMaps {
|
||||||
pub ipv6: TorrentMap<Ipv6Addr>,
|
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)]
|
#[derive(Default)]
|
||||||
pub struct Statistics {
|
pub struct Statistics {
|
||||||
pub requests_received: AtomicUsize,
|
pub requests_received: AtomicUsize,
|
||||||
|
|
@ -119,6 +179,7 @@ pub struct Statistics {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
pub access_list: Arc<AccessList>,
|
||||||
pub connections: Arc<Mutex<ConnectionMap>>,
|
pub connections: Arc<Mutex<ConnectionMap>>,
|
||||||
pub torrents: Arc<Mutex<TorrentMaps>>,
|
pub torrents: Arc<Mutex<TorrentMaps>>,
|
||||||
pub statistics: Arc<Statistics>,
|
pub statistics: Arc<Statistics>,
|
||||||
|
|
@ -127,6 +188,7 @@ pub struct State {
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
access_list: Arc::new(AccessList::default()),
|
||||||
connections: Arc::new(Mutex::new(HashMap::new())),
|
connections: Arc::new(Mutex::new(HashMap::new())),
|
||||||
torrents: Arc::new(Mutex::new(TorrentMaps::default())),
|
torrents: Arc::new(Mutex::new(TorrentMaps::default())),
|
||||||
statistics: Arc::new(Statistics::default()),
|
statistics: Arc::new(Statistics::default()),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessListConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use aquatic_cli_helpers::LogLevel;
|
use aquatic_cli_helpers::LogLevel;
|
||||||
|
|
@ -21,6 +22,7 @@ pub struct Config {
|
||||||
pub statistics: StatisticsConfig,
|
pub statistics: StatisticsConfig,
|
||||||
pub cleaning: CleaningConfig,
|
pub cleaning: CleaningConfig,
|
||||||
pub privileges: PrivilegeConfig,
|
pub privileges: PrivilegeConfig,
|
||||||
|
pub access_list: AccessListConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl aquatic_cli_helpers::Config for Config {
|
impl aquatic_cli_helpers::Config for Config {
|
||||||
|
|
@ -113,6 +115,7 @@ impl Default for Config {
|
||||||
statistics: StatisticsConfig::default(),
|
statistics: StatisticsConfig::default(),
|
||||||
cleaning: CleaningConfig::default(),
|
cleaning: CleaningConfig::default(),
|
||||||
privileges: PrivilegeConfig::default(),
|
privileges: PrivilegeConfig::default(),
|
||||||
|
access_list: AccessListConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
220
aquatic_udp/src/lib/handlers/announce.rs
Normal file
220
aquatic_udp/src/lib/handlers/announce.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
aquatic_udp/src/lib/handlers/connect.rs
Normal file
39
aquatic_udp/src/lib/handlers/connect.rs
Normal 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)
|
||||||
|
}));
|
||||||
|
}
|
||||||
148
aquatic_udp/src/lib/handlers/mod.rs
Normal file
148
aquatic_udp/src/lib/handlers/mod.rs
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
64
aquatic_udp/src/lib/handlers/scrape.rs
Normal file
64
aquatic_udp/src/lib/handlers/scrape.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,29 +23,42 @@ pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker";
|
||||||
pub fn run(config: Config) -> ::anyhow::Result<()> {
|
pub fn run(config: Config) -> ::anyhow::Result<()> {
|
||||||
let state = State::default();
|
let state = State::default();
|
||||||
|
|
||||||
|
tasks::update_access_list(&config, &state);
|
||||||
|
|
||||||
let num_bound_sockets = start_workers(config.clone(), state.clone())?;
|
let num_bound_sockets = start_workers(config.clone(), state.clone())?;
|
||||||
|
|
||||||
if config.privileges.drop_privileges {
|
if config.privileges.drop_privileges {
|
||||||
|
let mut counter = 0usize;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let sockets = num_bound_sockets.load(Ordering::SeqCst);
|
let sockets = num_bound_sockets.load(Ordering::SeqCst);
|
||||||
|
|
||||||
if sockets == config.socket_workers {
|
if sockets == config.socket_workers {
|
||||||
PrivDrop::default()
|
PrivDrop::default()
|
||||||
.chroot(config.privileges.chroot_path)
|
.chroot(config.privileges.chroot_path.clone())
|
||||||
.user(config.privileges.user)
|
.user(config.privileges.user.clone())
|
||||||
.apply()?;
|
.apply()?;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
::std::thread::sleep(Duration::from_millis(10));
|
::std::thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
counter += 1;
|
||||||
|
|
||||||
|
if counter == 500 {
|
||||||
|
panic!("Sockets didn't bind in time for privilege drop.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
|
::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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ fn read_requests(
|
||||||
let mut requests_received: usize = 0;
|
let mut requests_received: usize = 0;
|
||||||
let mut bytes_received: usize = 0;
|
let mut bytes_received: usize = 0;
|
||||||
|
|
||||||
|
let access_list_mode = config.access_list.mode;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match socket.recv_from(&mut buffer[..]) {
|
match socket.recv_from(&mut buffer[..]) {
|
||||||
Ok((amt, src)) => {
|
Ok((amt, src)) => {
|
||||||
|
|
@ -144,6 +146,18 @@ fn read_requests(
|
||||||
}
|
}
|
||||||
|
|
||||||
match request {
|
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) => {
|
Ok(request) => {
|
||||||
requests.push((request, src));
|
requests.push((request, src));
|
||||||
}
|
}
|
||||||
|
|
@ -152,9 +166,9 @@ fn read_requests(
|
||||||
|
|
||||||
if let Some(transaction_id) = err.transaction_id {
|
if let Some(transaction_id) = err.transaction_id {
|
||||||
let opt_message = if err.error.is_some() {
|
let opt_message = if err.error.is_some() {
|
||||||
Some("Parse error".to_string())
|
Some("Parse error".into())
|
||||||
} else if let Some(message) = err.message {
|
} else if let Some(message) = err.message {
|
||||||
Some(message)
|
Some(message.into())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,53 +3,29 @@ use std::time::Instant;
|
||||||
|
|
||||||
use histogram::Histogram;
|
use histogram::Histogram;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessListMode;
|
||||||
|
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
pub fn clean_connections_and_torrents(state: &State) {
|
pub fn update_access_list(config: &Config, state: &State) {
|
||||||
let now = Instant::now();
|
match config.access_list.mode {
|
||||||
|
AccessListMode::White | AccessListMode::Black => {
|
||||||
{
|
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
|
||||||
let mut connections = state.connections.lock();
|
::log::error!("Update access list from path: {:?}", err);
|
||||||
|
}
|
||||||
connections.retain(|_, v| v.0 > now);
|
}
|
||||||
connections.shrink_to_fit();
|
AccessListMode::Off => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut torrents = state.torrents.lock();
|
|
||||||
|
|
||||||
clean_torrent_map(&mut torrents.ipv4, now);
|
|
||||||
clean_torrent_map(&mut torrents.ipv6, now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
pub fn clean_connections(state: &State) {
|
||||||
fn clean_torrent_map<I: Ip>(torrents: &mut TorrentMap<I>, now: Instant) {
|
let now = Instant::now();
|
||||||
torrents.retain(|_, torrent| {
|
|
||||||
let num_seeders = &mut torrent.num_seeders;
|
|
||||||
let num_leechers = &mut torrent.num_leechers;
|
|
||||||
|
|
||||||
torrent.peers.retain(|_, peer| {
|
let mut connections = state.connections.lock();
|
||||||
let keep = peer.valid_until.0 > now;
|
|
||||||
|
|
||||||
if !keep {
|
connections.retain(|_, v| v.0 > now);
|
||||||
match peer.status {
|
connections.shrink_to_fit();
|
||||||
PeerStatus::Seeding => {
|
|
||||||
*num_seeders -= 1;
|
|
||||||
}
|
|
||||||
PeerStatus::Leeching => {
|
|
||||||
*num_leechers -= 1;
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
keep
|
|
||||||
});
|
|
||||||
|
|
||||||
!torrent.peers.is_empty()
|
|
||||||
});
|
|
||||||
|
|
||||||
torrents.shrink_to_fit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gather_and_print_statistics(state: &State, config: &Config) {
|
pub fn gather_and_print_statistics(state: &State, config: &Config) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::io::{self, Cursor, Write};
|
use std::io::{self, Cursor, Write};
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
|
@ -37,7 +38,7 @@ pub struct ScrapeResponse {
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
pub transaction_id: TransactionId,
|
pub transaction_id: TransactionId,
|
||||||
pub message: String,
|
pub message: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
|
|
@ -224,7 +225,9 @@ impl Response {
|
||||||
|
|
||||||
Ok((ErrorResponse {
|
Ok((ErrorResponse {
|
||||||
transaction_id: TransactionId(transaction_id),
|
transaction_id: TransactionId(transaction_id),
|
||||||
message: String::from_utf8_lossy(&inner[position..]).into(),
|
message: String::from_utf8_lossy(&inner[position..])
|
||||||
|
.into_owned()
|
||||||
|
.into(),
|
||||||
})
|
})
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +265,7 @@ impl Response {
|
||||||
}
|
}
|
||||||
_ => Ok((ErrorResponse {
|
_ => Ok((ErrorResponse {
|
||||||
transaction_id: TransactionId(transaction_id),
|
transaction_id: TransactionId(transaction_id),
|
||||||
message: "Invalid action".to_string(),
|
message: "Invalid action".into(),
|
||||||
})
|
})
|
||||||
.into()),
|
.into()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessList;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
@ -12,6 +14,8 @@ pub use aquatic_common::ValidUntil;
|
||||||
|
|
||||||
use aquatic_ws_protocol::*;
|
use aquatic_ws_protocol::*;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
pub const LISTENER_TOKEN: Token = Token(0);
|
pub const LISTENER_TOKEN: Token = Token(0);
|
||||||
pub const CHANNEL_TOKEN: Token = Token(1);
|
pub const CHANNEL_TOKEN: Token = Token(1);
|
||||||
|
|
||||||
|
|
@ -84,14 +88,62 @@ pub struct TorrentMaps {
|
||||||
pub ipv6: TorrentMap,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
pub access_list: Arc<AccessList>,
|
||||||
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
|
pub torrent_maps: Arc<Mutex<TorrentMaps>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for State {
|
impl Default for State {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
access_list: Arc::new(Default::default()),
|
||||||
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
|
torrent_maps: Arc::new(Mutex::new(TorrentMaps::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use aquatic_common::access_list::AccessListConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use aquatic_cli_helpers::LogLevel;
|
use aquatic_cli_helpers::LogLevel;
|
||||||
|
|
@ -21,6 +22,7 @@ pub struct Config {
|
||||||
pub cleaning: CleaningConfig,
|
pub cleaning: CleaningConfig,
|
||||||
pub statistics: StatisticsConfig,
|
pub statistics: StatisticsConfig,
|
||||||
pub privileges: PrivilegeConfig,
|
pub privileges: PrivilegeConfig,
|
||||||
|
pub access_list: AccessListConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl aquatic_cli_helpers::Config for Config {
|
impl aquatic_cli_helpers::Config for Config {
|
||||||
|
|
@ -105,6 +107,7 @@ impl Default for Config {
|
||||||
cleaning: CleaningConfig::default(),
|
cleaning: CleaningConfig::default(),
|
||||||
statistics: StatisticsConfig::default(),
|
statistics: StatisticsConfig::default(),
|
||||||
privileges: PrivilegeConfig::default(),
|
privileges: PrivilegeConfig::default(),
|
||||||
|
access_list: AccessListConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,16 @@ pub const APP_NAME: &str = "aquatic_ws: WebTorrent tracker";
|
||||||
pub fn run(config: Config) -> anyhow::Result<()> {
|
pub fn run(config: Config) -> anyhow::Result<()> {
|
||||||
let state = State::default();
|
let state = State::default();
|
||||||
|
|
||||||
|
tasks::update_access_list(&config, &state);
|
||||||
|
|
||||||
start_workers(config.clone(), state.clone())?;
|
start_workers(config.clone(), state.clone())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
::std::thread::sleep(Duration::from_secs(config.cleaning.interval));
|
::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 {
|
for i in 0..config.socket_workers {
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
let state = state.clone();
|
||||||
let socket_worker_statuses = socket_worker_statuses.clone();
|
let socket_worker_statuses = socket_worker_statuses.clone();
|
||||||
let in_message_sender = in_message_sender.clone();
|
let in_message_sender = in_message_sender.clone();
|
||||||
let opt_tls_acceptor = opt_tls_acceptor.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 || {
|
.spawn(move || {
|
||||||
network::run_socket_worker(
|
network::run_socket_worker(
|
||||||
config,
|
config,
|
||||||
|
state,
|
||||||
i,
|
i,
|
||||||
socket_worker_statuses,
|
socket_worker_statuses,
|
||||||
poll,
|
poll,
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ pub enum Stream {
|
||||||
|
|
||||||
impl Stream {
|
impl Stream {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_peer_addr(&self) -> SocketAddr {
|
pub fn get_peer_addr(&self) -> ::std::io::Result<SocketAddr> {
|
||||||
match self {
|
match self {
|
||||||
Self::TcpStream(stream) => stream.peer_addr().unwrap(),
|
Self::TcpStream(stream) => stream.peer_addr(),
|
||||||
Self::TlsStream(stream) => stream.get_ref().peer_addr().unwrap(),
|
Self::TlsStream(stream) => stream.get_ref().peer_addr(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ impl HandshakeMachine {
|
||||||
);
|
);
|
||||||
|
|
||||||
(Some(Either::Right(Self::TlsStream(stream))), false)
|
(Some(Either::Right(Self::TlsStream(stream))), false)
|
||||||
},
|
}
|
||||||
Err(native_tls::HandshakeError::WouldBlock(handshake)) => {
|
Err(native_tls::HandshakeError::WouldBlock(handshake)) => {
|
||||||
(Some(Either::Right(Self::TlsMidHandshake(handshake))), true)
|
(Some(Either::Right(Self::TlsMidHandshake(handshake))), true)
|
||||||
}
|
}
|
||||||
|
|
@ -162,21 +162,26 @@ impl HandshakeMachine {
|
||||||
result: Result<WebSocket<Stream>, HandshakeError<ServerHandshake<Stream, NoCallback>>>,
|
result: Result<WebSocket<Stream>, HandshakeError<ServerHandshake<Stream, NoCallback>>>,
|
||||||
) -> (Option<Either<EstablishedWs, Self>>, bool) {
|
) -> (Option<Either<EstablishedWs, Self>>, bool) {
|
||||||
match result {
|
match result {
|
||||||
Ok(mut ws) => {
|
Ok(mut ws) => match ws.get_mut().get_peer_addr() {
|
||||||
let peer_addr = ws.get_mut().get_peer_addr();
|
Ok(peer_addr) => {
|
||||||
|
::log::trace!(
|
||||||
|
"established ws handshake with peer with addr: {:?}",
|
||||||
|
peer_addr
|
||||||
|
);
|
||||||
|
|
||||||
::log::trace!(
|
let established_ws = EstablishedWs { ws, peer_addr };
|
||||||
"established ws handshake with peer with addr: {:?}",
|
|
||||||
peer_addr
|
|
||||||
);
|
|
||||||
|
|
||||||
let established_ws = EstablishedWs {
|
(Some(Either::Left(established_ws)), false)
|
||||||
ws,
|
}
|
||||||
peer_addr,
|
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)) => (
|
Err(HandshakeError::Interrupted(handshake)) => (
|
||||||
Some(Either::Right(HandshakeMachine::WsMidHandshake(handshake))),
|
Some(Either::Right(HandshakeMachine::WsMidHandshake(handshake))),
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::vec::Drain;
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
|
|
@ -23,6 +24,7 @@ use utils::*;
|
||||||
|
|
||||||
pub fn run_socket_worker(
|
pub fn run_socket_worker(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
state: State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
socket_worker_statuses: SocketWorkerStatuses,
|
socket_worker_statuses: SocketWorkerStatuses,
|
||||||
poll: Poll,
|
poll: Poll,
|
||||||
|
|
@ -36,6 +38,7 @@ pub fn run_socket_worker(
|
||||||
|
|
||||||
run_poll_loop(
|
run_poll_loop(
|
||||||
config,
|
config,
|
||||||
|
&state,
|
||||||
socket_worker_index,
|
socket_worker_index,
|
||||||
poll,
|
poll,
|
||||||
in_message_sender,
|
in_message_sender,
|
||||||
|
|
@ -53,6 +56,7 @@ pub fn run_socket_worker(
|
||||||
|
|
||||||
pub fn run_poll_loop(
|
pub fn run_poll_loop(
|
||||||
config: Config,
|
config: Config,
|
||||||
|
state: &State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
mut poll: Poll,
|
mut poll: Poll,
|
||||||
in_message_sender: InMessageSender,
|
in_message_sender: InMessageSender,
|
||||||
|
|
@ -76,6 +80,7 @@ pub fn run_poll_loop(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut connections: ConnectionMap = HashMap::new();
|
let mut connections: ConnectionMap = HashMap::new();
|
||||||
|
let mut local_responses = Vec::new();
|
||||||
|
|
||||||
let mut poll_token_counter = Token(0usize);
|
let mut poll_token_counter = Token(0usize);
|
||||||
let mut iter_counter = 0usize;
|
let mut iter_counter = 0usize;
|
||||||
|
|
@ -100,7 +105,10 @@ pub fn run_poll_loop(
|
||||||
);
|
);
|
||||||
} else if token != CHANNEL_TOKEN {
|
} else if token != CHANNEL_TOKEN {
|
||||||
run_handshakes_and_read_messages(
|
run_handshakes_and_read_messages(
|
||||||
|
&config,
|
||||||
|
state,
|
||||||
socket_worker_index,
|
socket_worker_index,
|
||||||
|
&mut local_responses,
|
||||||
&in_message_sender,
|
&in_message_sender,
|
||||||
&opt_tls_acceptor,
|
&opt_tls_acceptor,
|
||||||
&mut poll,
|
&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
|
// 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
|
/// On the stream given by poll_token, get TLS (if requested) and tungstenite
|
||||||
/// up and running, then read messages and pass on through channel.
|
/// up and running, then read messages and pass on through channel.
|
||||||
pub fn run_handshakes_and_read_messages(
|
pub fn run_handshakes_and_read_messages(
|
||||||
|
config: &Config,
|
||||||
|
state: &State,
|
||||||
socket_worker_index: usize,
|
socket_worker_index: usize,
|
||||||
|
local_responses: &mut Vec<(ConnectionMeta, OutMessage)>,
|
||||||
in_message_sender: &InMessageSender,
|
in_message_sender: &InMessageSender,
|
||||||
opt_tls_acceptor: &Option<TlsAcceptor>, // If set, run TLS
|
opt_tls_acceptor: &Option<TlsAcceptor>, // If set, run TLS
|
||||||
poll: &mut Poll,
|
poll: &mut Poll,
|
||||||
|
|
@ -173,6 +189,8 @@ pub fn run_handshakes_and_read_messages(
|
||||||
poll_token: Token,
|
poll_token: Token,
|
||||||
valid_until: ValidUntil,
|
valid_until: ValidUntil,
|
||||||
) {
|
) {
|
||||||
|
let access_list_mode = config.access_list.mode;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(established_ws) = connections
|
if let Some(established_ws) = connections
|
||||||
.get_mut(&poll_token)
|
.get_mut(&poll_token)
|
||||||
|
|
@ -188,21 +206,50 @@ pub fn run_handshakes_and_read_messages(
|
||||||
|
|
||||||
match established_ws.ws.read_message() {
|
match established_ws.ws.read_message() {
|
||||||
Ok(ws_message) => {
|
Ok(ws_message) => {
|
||||||
if let Ok(in_message) = InMessage::from_ws_message(ws_message) {
|
let naive_peer_addr = established_ws.peer_addr;
|
||||||
let naive_peer_addr = established_ws.peer_addr;
|
let converted_peer_ip = convert_ipv4_mapped_ipv6(naive_peer_addr.ip());
|
||||||
let converted_peer_ip = convert_ipv4_mapped_ipv6(naive_peer_addr.ip());
|
|
||||||
|
|
||||||
let meta = ConnectionMeta {
|
let meta = ConnectionMeta {
|
||||||
worker_index: socket_worker_index,
|
worker_index: socket_worker_index,
|
||||||
poll_token,
|
poll_token,
|
||||||
naive_peer_addr,
|
naive_peer_addr,
|
||||||
converted_peer_ip,
|
converted_peer_ip,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("read message");
|
debug!("read message");
|
||||||
|
|
||||||
if let Err(err) = in_message_sender.send((meta, in_message)) {
|
match InMessage::from_ws_message(ws_message) {
|
||||||
error!("InMessageSender: couldn't send message: {:?}", err);
|
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
|
/// Read messages from channel, send to peers
|
||||||
pub fn send_out_messages(
|
pub fn send_out_messages(
|
||||||
poll: &mut Poll,
|
poll: &mut Poll,
|
||||||
|
local_responses: Drain<(ConnectionMeta, OutMessage)>,
|
||||||
out_message_receiver: &Receiver<(ConnectionMeta, OutMessage)>,
|
out_message_receiver: &Receiver<(ConnectionMeta, OutMessage)>,
|
||||||
connections: &mut ConnectionMap,
|
connections: &mut ConnectionMap,
|
||||||
) {
|
) {
|
||||||
let len = out_message_receiver.len();
|
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
|
let opt_established_ws = connections
|
||||||
.get_mut(&meta.poll_token)
|
.get_mut(&meta.poll_token)
|
||||||
.and_then(Connection::get_established_ws);
|
.and_then(Connection::get_established_ws);
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,18 @@
|
||||||
use std::time::Instant;
|
use aquatic_common::access_list::AccessListMode;
|
||||||
|
|
||||||
use histogram::Histogram;
|
use histogram::Histogram;
|
||||||
|
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
pub fn clean_torrents(state: &State) {
|
pub fn update_access_list(config: &Config, state: &State) {
|
||||||
fn clean_torrent_map(torrent_map: &mut TorrentMap) {
|
match config.access_list.mode {
|
||||||
let now = Instant::now();
|
AccessListMode::White | AccessListMode::Black => {
|
||||||
|
if let Err(err) = state.access_list.update_from_path(&config.access_list.path) {
|
||||||
torrent_map.retain(|_, torrent_data| {
|
::log::error!("Couldn't update access list: {:?}", err);
|
||||||
let num_seeders = &mut torrent_data.num_seeders;
|
}
|
||||||
let num_leechers = &mut torrent_data.num_leechers;
|
}
|
||||||
|
AccessListMode::Off => {}
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
pub fn print_statistics(state: &State) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub struct Statistics {
|
||||||
pub responses_offer: AtomicUsize,
|
pub responses_offer: AtomicUsize,
|
||||||
pub responses_answer: AtomicUsize,
|
pub responses_answer: AtomicUsize,
|
||||||
pub responses_scrape: AtomicUsize,
|
pub responses_scrape: AtomicUsize,
|
||||||
|
pub responses_error: AtomicUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
|
||||||
|
|
@ -85,13 +85,16 @@ fn monitor_statistics(state: LoadTestState, config: &Config) {
|
||||||
statistics.responses_answer.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
|
statistics.responses_answer.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
|
||||||
let responses_scrape_per_second =
|
let responses_scrape_per_second =
|
||||||
statistics.responses_scrape.fetch_and(0, Ordering::SeqCst) as f64 / interval_f64;
|
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_announce_per_second = responses_announce / interval_f64;
|
||||||
|
|
||||||
let responses_per_second = responses_announce_per_second
|
let responses_per_second = responses_announce_per_second
|
||||||
+ responses_offer_per_second
|
+ responses_offer_per_second
|
||||||
+ responses_answer_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);
|
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!(" - Offer responses: {:.2}", responses_offer_per_second);
|
||||||
println!(" - Answer responses: {:.2}", responses_answer_per_second);
|
println!(" - Answer responses: {:.2}", responses_answer_per_second);
|
||||||
println!(" - Scrape responses: {:.2}", responses_scrape_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 time_elapsed = start_time.elapsed();
|
||||||
let duration = Duration::from_secs(config.duration as u64);
|
let duration = Duration::from_secs(config.duration as u64);
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,16 @@ impl Connection {
|
||||||
|
|
||||||
self.can_send = true;
|
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) => {
|
Err(err) => {
|
||||||
eprintln!("error deserializing offer: {:?}", err);
|
eprintln!("error deserializing offer: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
|
|
@ -102,7 +177,7 @@ impl<'de> Visitor<'de> for TwentyByteVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
|
|
@ -113,7 +188,7 @@ where
|
||||||
mod tests {
|
mod tests {
|
||||||
use quickcheck_macros::quickcheck;
|
use quickcheck_macros::quickcheck;
|
||||||
|
|
||||||
use crate::InfoHash;
|
use crate::common::InfoHash;
|
||||||
|
|
||||||
fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash {
|
fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash {
|
||||||
let mut arr = [0u8; 20];
|
let mut arr = [0u8; 20];
|
||||||
|
|
@ -1,281 +1,22 @@
|
||||||
use anyhow::Context;
|
//! WebTorrent protocol implementation
|
||||||
use hashbrown::HashMap;
|
//!
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
//! 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::*;
|
pub use common::*;
|
||||||
|
pub use request::*;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
pub use response::*;
|
||||||
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)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
||||||
60
aquatic_ws_protocol/src/request/announce.rs
Normal file
60
aquatic_ws_protocol/src/request/announce.rs
Normal 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>,
|
||||||
|
}
|
||||||
36
aquatic_ws_protocol/src/request/mod.rs
Normal file
36
aquatic_ws_protocol/src/request/mod.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
29
aquatic_ws_protocol/src/request/scrape.rs
Normal file
29
aquatic_ws_protocol/src/request/scrape.rs
Normal 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>,
|
||||||
|
}
|
||||||
14
aquatic_ws_protocol/src/response/announce.rs
Normal file
14
aquatic_ws_protocol/src/response/announce.rs
Normal 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
|
||||||
|
}
|
||||||
16
aquatic_ws_protocol/src/response/answer.rs
Normal file
16
aquatic_ws_protocol/src/response/answer.rs
Normal 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,
|
||||||
|
}
|
||||||
24
aquatic_ws_protocol/src/response/error.rs
Normal file
24
aquatic_ws_protocol/src/response/error.rs
Normal 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>,
|
||||||
|
}
|
||||||
44
aquatic_ws_protocol/src/response/mod.rs
Normal file
44
aquatic_ws_protocol/src/response/mod.rs
Normal 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)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
aquatic_ws_protocol/src/response/offer.rs
Normal file
19
aquatic_ws_protocol/src/response/offer.rs
Normal 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,
|
||||||
|
}
|
||||||
19
aquatic_ws_protocol/src/response/scrape.rs
Normal file
19
aquatic_ws_protocol/src/response/scrape.rs
Normal 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,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue