initial commit
This commit is contained in:
commit
b32f00499a
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.vscode/
|
||||||
|
target/
|
558
Cargo.lock
generated
Normal file
558
Cargo.lock
generated
Normal file
@ -0,0 +1,558 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flume"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"nanorand",
|
||||||
|
"spin",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heapless"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||||
|
dependencies = [
|
||||||
|
"hash32",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.77"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.174"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "meteoro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"flume",
|
||||||
|
"heapless",
|
||||||
|
"log",
|
||||||
|
"rand",
|
||||||
|
"simplelog",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nanorand"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.219"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplelog"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"termcolor",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"libc",
|
||||||
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.2+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rt"
|
||||||
|
version = "0.39.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "meteoro"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
flume = { version = "0.11.1", features = ["eventual-fairness", "select"] }
|
||||||
|
heapless = "0.8.0"
|
||||||
|
log = { version = "0.4.27", features = ["release_max_level_info"] }
|
||||||
|
rand = "0.9.2"
|
||||||
|
simplelog = "0.12.2"
|
12
src/constants.rs
Normal file
12
src/constants.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
pub const METEORO_HELP_TEXT: &'static str = "Usage:\tmeteoro [OPTIONS]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
|
||||||
|
-a IPV4_ADDRESS Provide an IPv4 address to bind to
|
||||||
|
(example: -a 192.168.1.2:6969)
|
||||||
|
|
||||||
|
-A IPV6_ADDRESS Provide an IPv6 address to bind to
|
||||||
|
(example: -A [::1]:6970)
|
||||||
|
|
||||||
|
-h | --help Show this help text
|
||||||
|
-v | --version Print the program's build & version information";
|
89
src/main.rs
Normal file
89
src/main.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||||
|
process::ExitCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::LevelFilter;
|
||||||
|
use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
|
||||||
|
|
||||||
|
mod constants;
|
||||||
|
mod tracker;
|
||||||
|
use tracker::Tracker;
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let log_level: LevelFilter = LevelFilter::Trace;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let log_level: LevelFilter = LevelFilter::Info;
|
||||||
|
|
||||||
|
let logger_config = ConfigBuilder::new()
|
||||||
|
.set_target_level(LevelFilter::Off)
|
||||||
|
.set_location_level(LevelFilter::Off)
|
||||||
|
.set_thread_level(LevelFilter::Off)
|
||||||
|
.set_time_level(LevelFilter::Info)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TermLogger::init(
|
||||||
|
log_level,
|
||||||
|
logger_config,
|
||||||
|
TerminalMode::Mixed,
|
||||||
|
ColorChoice::Auto,
|
||||||
|
)
|
||||||
|
.expect("failed to initialise terminal logging");
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
if args.contains(&String::from("--help")) || args.contains(&String::from("-h")) {
|
||||||
|
println!("{}", constants::METEORO_HELP_TEXT);
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.contains(&String::from("--version")) || args.contains(&String::from("-v")) {
|
||||||
|
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ipv4_flag_idxs: Vec<usize> = args
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, s)| if s == "-a" { Some(idx) } else { None })
|
||||||
|
.collect();
|
||||||
|
let ipv6_flag_idxs: Vec<usize> = args
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(idx, s)| if s == "-A" { Some(idx) } else { None })
|
||||||
|
.collect();
|
||||||
|
let mut ip_addrs: Vec<SocketAddr> = ipv4_flag_idxs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|idx| {
|
||||||
|
Some(SocketAddr::V4(
|
||||||
|
args.get(*idx + 1)?.parse::<SocketAddrV4>().ok()?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
ip_addrs.extend(ipv6_flag_idxs.iter().filter_map(|idx| {
|
||||||
|
Some(SocketAddr::V6(
|
||||||
|
args.get(*idx + 1)?.parse::<SocketAddrV6>().ok()?,
|
||||||
|
))
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ip_addrs.is_empty() {
|
||||||
|
ip_addrs.push(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 6969)));
|
||||||
|
ip_addrs.push(SocketAddr::V6(SocketAddrV6::new(
|
||||||
|
Ipv6Addr::LOCALHOST,
|
||||||
|
6969,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tracker = Tracker::new();
|
||||||
|
return match tracker.start_on(&ip_addrs) {
|
||||||
|
Ok(_) => ExitCode::SUCCESS,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Fatal error: {}", e);
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
22
src/tracker/bittorrent/info_hash.rs
Normal file
22
src/tracker/bittorrent/info_hash.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
pub const INFO_HASH_SIZE: usize = 20;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
|
||||||
|
pub struct InfoHash {
|
||||||
|
pub bytes: [u8; INFO_HASH_SIZE],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for InfoHash {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
const INFO_HASH_STR_LEN: usize = INFO_HASH_SIZE * 2;
|
||||||
|
let mut hex_string: heapless::String<INFO_HASH_STR_LEN> = heapless::String::new();
|
||||||
|
|
||||||
|
for b in self.bytes {
|
||||||
|
let _ = write!(hex_string, "{:02x}", b);
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{}", hex_string)
|
||||||
|
}
|
||||||
|
}
|
4
src/tracker/bittorrent/mod.rs
Normal file
4
src/tracker/bittorrent/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod info_hash;
|
||||||
|
pub mod peer;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod udp;
|
20
src/tracker/bittorrent/peer.rs
Normal file
20
src/tracker/bittorrent/peer.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
const MAX_PEER_ID_LENGTH: usize = 20;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PeerId {
|
||||||
|
pub bytes: [u8; MAX_PEER_ID_LENGTH],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PeerId {
|
||||||
|
pub fn to_string_lossy(&self) -> String {
|
||||||
|
String::from_utf8_lossy(&self.bytes).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PeerId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.to_string_lossy())
|
||||||
|
}
|
||||||
|
}
|
62
src/tracker/bittorrent/protocol.rs
Normal file
62
src/tracker/bittorrent/protocol.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/// BitTorrent's UDP magic `connection_id` constant
|
||||||
|
pub const UDP_MAGIC: i64 = 0x41727101980;
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum Action {
|
||||||
|
Connect = 0,
|
||||||
|
Announce = 1,
|
||||||
|
Scrape = 2,
|
||||||
|
Error = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Action {
|
||||||
|
pub fn from_i32(val: i32) -> Option<Action> {
|
||||||
|
match val {
|
||||||
|
0 => Some(Action::Connect),
|
||||||
|
1 => Some(Action::Announce),
|
||||||
|
2 => Some(Action::Scrape),
|
||||||
|
3 => Some(Action::Error),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_i32(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Action::Connect => 0,
|
||||||
|
Action::Announce => 1,
|
||||||
|
Action::Scrape => 2,
|
||||||
|
Action::Error => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(i32)]
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum Event {
|
||||||
|
None = 0,
|
||||||
|
Completed = 1,
|
||||||
|
Started = 2,
|
||||||
|
Stopped = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Event {
|
||||||
|
pub fn from_i32(val: i32) -> Option<Event> {
|
||||||
|
match val {
|
||||||
|
0 => Some(Event::None),
|
||||||
|
1 => Some(Event::Completed),
|
||||||
|
2 => Some(Event::Started),
|
||||||
|
3 => Some(Event::Stopped),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_i32(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Event::None => 0,
|
||||||
|
Event::Completed => 1,
|
||||||
|
Event::Started => 2,
|
||||||
|
Event::Stopped => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
213
src/tracker/bittorrent/udp.rs
Normal file
213
src/tracker/bittorrent/udp.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
io::{Cursor, Write},
|
||||||
|
net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tracker::bittorrent::{
|
||||||
|
info_hash::InfoHash,
|
||||||
|
peer::PeerId,
|
||||||
|
protocol::{Action, Event},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MIN_ANNOUNCE_REQUEST_SIZE: usize = 98;
|
||||||
|
pub const MIN_SCRAPE_REQUEST_SIZE: usize = 36;
|
||||||
|
pub const MIN_CONNECTION_RESPONSE_SIZE: usize = 16;
|
||||||
|
pub const MIN_ANNOUNCE_RESPONSE_SIZE: usize = 20;
|
||||||
|
pub const MIN_SCRAPE_RESPONSE_SIZE: usize = 8;
|
||||||
|
pub const SCRAPE_RESPONSE_ENTRY_SIZE: usize = 12;
|
||||||
|
|
||||||
|
pub const IPV4_SIZE: usize = Ipv4Addr::BITS as usize / 8;
|
||||||
|
pub const IPV6_SIZE: usize = Ipv6Addr::BITS as usize / 8;
|
||||||
|
pub const PORT_SIZE: usize = size_of::<u16>();
|
||||||
|
pub const IPV4_ADDR_PAIR_SIZE: usize = IPV4_SIZE + PORT_SIZE;
|
||||||
|
pub const IPV6_ADDR_PAIR_SIZE: usize = IPV6_SIZE + PORT_SIZE;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConnectRequest {
|
||||||
|
pub protocol_id: i64,
|
||||||
|
pub transaction_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConnectResponse {
|
||||||
|
pub transaction_id: i32,
|
||||||
|
pub connection_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectResponse {
|
||||||
|
pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
|
||||||
|
let mut c = Cursor::new(buf);
|
||||||
|
let mut written: usize = 0;
|
||||||
|
|
||||||
|
const ACTION: i32 = Action::Connect as i32;
|
||||||
|
|
||||||
|
written += c.write(&ACTION.to_be_bytes()).unwrap();
|
||||||
|
written += c.write(&self.transaction_id.to_be_bytes()).unwrap();
|
||||||
|
written += c.write(&self.connection_id.to_be_bytes()).unwrap();
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnnounceRequest {
|
||||||
|
pub connection_id: i64,
|
||||||
|
pub transaction_id: i32,
|
||||||
|
/// Info hash of the torrent
|
||||||
|
pub info_hash: InfoHash,
|
||||||
|
/// Peer's ID
|
||||||
|
pub peer_id: PeerId,
|
||||||
|
/// Number of bytes peer has downloaded this session
|
||||||
|
pub downloaded: i64,
|
||||||
|
/// Number of bytes peer has remaining to complete the download
|
||||||
|
pub left: i64,
|
||||||
|
/// Number of bytes peer has uploaded this session
|
||||||
|
pub uploaded: i64,
|
||||||
|
/// One of 4 announce events. `Event::None` is used to update presence but not change peer's state.
|
||||||
|
pub event: Event,
|
||||||
|
/// Peer's desired IPv4 address to be sent the announce response on. `None` implies that the packet's source address should be used.
|
||||||
|
pub ipv4_address: Option<Ipv4Addr>,
|
||||||
|
/// Client's unique key for tracking a single client over multiple connections (sockets)
|
||||||
|
pub key: u32,
|
||||||
|
/// Maximum number of info hashes the client is requesting. `None` implies a tracker-defined default value should be used.
|
||||||
|
pub num_want: Option<u32>,
|
||||||
|
/// Peer's desired port to be the announce response on. If `ipv4_address` is `Some` then this should be used.
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum AnnounceResponse {
|
||||||
|
V4(AnnounceResponseV4),
|
||||||
|
V6(AnnounceResponseV6),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnnounceResponseCommon {
|
||||||
|
pub transaction_id: i32,
|
||||||
|
/// Number of seconds peer should wait before re-announcing
|
||||||
|
pub interval: i32,
|
||||||
|
/// Number of peers still downloading
|
||||||
|
pub leechers: i32,
|
||||||
|
/// Number of peers who have completed the download and are seeding
|
||||||
|
pub seeders: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnnounceResponseV4 {
|
||||||
|
pub inner: AnnounceResponseCommon,
|
||||||
|
pub peers: Vec<SocketAddrV4>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AnnounceResponseV6 {
|
||||||
|
pub inner: AnnounceResponseCommon,
|
||||||
|
pub peers: Vec<SocketAddrV6>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnnounceResponseCommon {
|
||||||
|
pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
|
||||||
|
const ACTION: i32 = Action::Announce as i32;
|
||||||
|
buf[0..4].copy_from_slice(&ACTION.to_be_bytes());
|
||||||
|
buf[4..8].copy_from_slice(&self.transaction_id.to_be_bytes());
|
||||||
|
buf[8..12].copy_from_slice(&self.interval.to_be_bytes());
|
||||||
|
buf[12..16].copy_from_slice(&self.leechers.to_be_bytes());
|
||||||
|
buf[16..20].copy_from_slice(&self.seeders.to_be_bytes());
|
||||||
|
|
||||||
|
return MIN_ANNOUNCE_RESPONSE_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnnounceResponseV4 {
|
||||||
|
pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
|
||||||
|
let written = self.inner.write_to_buf(buf);
|
||||||
|
|
||||||
|
for (offset, addr) in (written..buf.len())
|
||||||
|
.step_by(IPV4_ADDR_PAIR_SIZE)
|
||||||
|
.zip(&self.peers)
|
||||||
|
{
|
||||||
|
buf[offset..offset + IPV4_SIZE].copy_from_slice(&addr.ip().octets());
|
||||||
|
buf[offset + IPV4_SIZE..offset + IPV4_ADDR_PAIR_SIZE]
|
||||||
|
.copy_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
return written + self.peers.len() * IPV4_ADDR_PAIR_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnnounceResponseV6 {
|
||||||
|
pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
|
||||||
|
let written = self.inner.write_to_buf(buf);
|
||||||
|
|
||||||
|
for (offset, addr) in (written..buf.len())
|
||||||
|
.step_by(IPV6_ADDR_PAIR_SIZE)
|
||||||
|
.zip(&self.peers)
|
||||||
|
{
|
||||||
|
buf[offset..offset + IPV6_SIZE].copy_from_slice(&addr.ip().octets());
|
||||||
|
buf[offset + IPV6_SIZE..offset + IPV6_ADDR_PAIR_SIZE]
|
||||||
|
.copy_from_slice(&addr.port().to_be_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
return written + self.peers.len() * IPV6_ADDR_PAIR_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScrapeRequest {
|
||||||
|
pub connection_id: i64,
|
||||||
|
pub transaction_id: i32,
|
||||||
|
pub info_hashes: Vec<InfoHash>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ScrapeStats {
|
||||||
|
/// Number of connected peers who have completed the download and are seeding
|
||||||
|
pub seeders: i32,
|
||||||
|
/// Number of peers who have ever completed the download
|
||||||
|
pub completed: i32,
|
||||||
|
/// Number of connected peers who have not completed the download and are seeding
|
||||||
|
pub leechers: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScrapeResponse {
|
||||||
|
pub transaction_id: i32,
|
||||||
|
pub stats: Vec<ScrapeStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrapeResponse {
|
||||||
|
pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
|
||||||
|
let mut c = Cursor::new(buf);
|
||||||
|
let mut written: usize = 0;
|
||||||
|
|
||||||
|
const ACTION: i32 = Action::Scrape as i32;
|
||||||
|
written += c.write(&ACTION.to_be_bytes()).unwrap();
|
||||||
|
written += c.write(&self.transaction_id.to_be_bytes()).unwrap();
|
||||||
|
|
||||||
|
for s in &self.stats {
|
||||||
|
written += c.write(&s.seeders.to_be_bytes()).unwrap();
|
||||||
|
written += c.write(&s.completed.to_be_bytes()).unwrap();
|
||||||
|
written += c.write(&s.leechers.to_be_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub transaction_id: i32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UdpRequest {
|
||||||
|
Connect(ConnectRequest),
|
||||||
|
Announce(AnnounceRequest),
|
||||||
|
Scrape(ScrapeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UdpResponse {
|
||||||
|
Connect(ConnectResponse),
|
||||||
|
Announce(AnnounceResponse),
|
||||||
|
Scrape(ScrapeResponse),
|
||||||
|
Error(ErrorResponse),
|
||||||
|
}
|
5
src/tracker/mod.rs
Normal file
5
src/tracker/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod bittorrent;
|
||||||
|
mod tracker;
|
||||||
|
mod udp_server;
|
||||||
|
|
||||||
|
pub use tracker::Tracker;
|
427
src/tracker/tracker.rs
Normal file
427
src/tracker/tracker.rs
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use flume::Sender;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::tracker::bittorrent::info_hash::InfoHash;
|
||||||
|
use crate::tracker::bittorrent::protocol::Event;
|
||||||
|
use crate::tracker::bittorrent::udp::{
|
||||||
|
AnnounceResponse, AnnounceResponseCommon, AnnounceResponseV4, AnnounceResponseV6,
|
||||||
|
ConnectResponse, ScrapeResponse, ScrapeStats, UdpRequest, UdpResponse,
|
||||||
|
};
|
||||||
|
use crate::tracker::udp_server::UdpServer;
|
||||||
|
|
||||||
|
pub struct RequestMessage {
|
||||||
|
pub(crate) request: UdpRequest,
|
||||||
|
pub(crate) src_addr: SocketAddr,
|
||||||
|
pub(crate) server_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResponseMessage {
|
||||||
|
pub(crate) response: UdpResponse,
|
||||||
|
pub(crate) dst_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PeerMetadata {
|
||||||
|
socket_addr: SocketAddr,
|
||||||
|
last_active: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PeerStatus {
|
||||||
|
connection_id: i64,
|
||||||
|
last_event: Event,
|
||||||
|
last_active: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const GARBAGE_COLLECTION_INTERVAL: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
pub const CONNECTION_EXPIRE_TIME: Duration = Duration::from_secs(90);
|
||||||
|
pub const DEFAULT_ANNOUNCE_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
pub const DEFAULT_ANNOUNCE_WANT: u32 = 80;
|
||||||
|
|
||||||
|
type ConnectionIdMap = HashMap<i64, PeerMetadata>;
|
||||||
|
type InfoHashMap = HashMap<InfoHash, Vec<PeerStatus>>;
|
||||||
|
|
||||||
|
pub static SHOULD_STOP_SIGNAL: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub struct Tracker {
|
||||||
|
server_threads: Vec<thread::JoinHandle<()>>,
|
||||||
|
response_channels: Vec<Sender<ResponseMessage>>,
|
||||||
|
/// Map of IPs & ports to peer connection IDs
|
||||||
|
peers: Arc<RwLock<ConnectionIdMap>>,
|
||||||
|
info_hashes: Arc<RwLock<InfoHashMap>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tracker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Tracker {
|
||||||
|
server_threads: Vec::new(),
|
||||||
|
response_channels: Vec::new(),
|
||||||
|
peers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
info_hashes: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_on(&mut self, addrs: &[SocketAddr]) -> Result<()> {
|
||||||
|
if addrs.is_empty() {
|
||||||
|
log::error!("No addresses provided for tracker to listen on");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only one request channel set is necessary as all the server threads send on the same one and the tracker is the only reader
|
||||||
|
let (req_send, req_recv) = flume::unbounded::<RequestMessage>();
|
||||||
|
|
||||||
|
let mut server_id: u32 = 0;
|
||||||
|
|
||||||
|
for a in addrs {
|
||||||
|
// Each UDP server gets its own response channels
|
||||||
|
let (resp_send, resp_recv) = flume::unbounded::<ResponseMessage>();
|
||||||
|
let req_chan = req_send.clone();
|
||||||
|
let a = a.clone();
|
||||||
|
|
||||||
|
self.server_threads.push(
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!(
|
||||||
|
"{} server thread (ID {})",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
server_id
|
||||||
|
))
|
||||||
|
.spawn(move || {
|
||||||
|
let server = UdpServer::new(a, server_id);
|
||||||
|
loop {
|
||||||
|
match server.start(req_chan.clone(), resp_recv.clone()) {
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to start server {}: {}", server_id, e)
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.response_channels.push(resp_send);
|
||||||
|
|
||||||
|
server_id += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gc_peers = self.peers.clone();
|
||||||
|
let gc_info_hashes = self.info_hashes.clone();
|
||||||
|
let _gc_thread = thread::spawn(move || {
|
||||||
|
let peers = gc_peers;
|
||||||
|
let info_hashes = gc_info_hashes;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
thread::sleep(GARBAGE_COLLECTION_INTERVAL);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
let expired = Tracker::purge_expired_connections(&mut peers.write().unwrap());
|
||||||
|
Tracker::purge_expired_swarm_peers(&mut info_hashes.write().unwrap(), &expired);
|
||||||
|
|
||||||
|
log::trace!("Garbage collection took {} ns", start.elapsed().as_nanos())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let request = match req_recv.recv() {
|
||||||
|
Ok(r) => r,
|
||||||
|
_ => {
|
||||||
|
log::error!("Internal tracker error: server request channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match request.request {
|
||||||
|
UdpRequest::Connect(connect) => {
|
||||||
|
log::trace!(
|
||||||
|
"(Server {}) Connect request: {:?}",
|
||||||
|
request.server_id,
|
||||||
|
connect
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_id: i64 = rng.random();
|
||||||
|
|
||||||
|
let mut peers = self.peers.write().unwrap();
|
||||||
|
|
||||||
|
peers.insert(
|
||||||
|
new_id,
|
||||||
|
PeerMetadata {
|
||||||
|
socket_addr: request.src_addr,
|
||||||
|
last_active: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match self.response_channels[request.server_id as usize].send(ResponseMessage {
|
||||||
|
response: UdpResponse::Connect(ConnectResponse {
|
||||||
|
transaction_id: connect.transaction_id,
|
||||||
|
connection_id: new_id,
|
||||||
|
}),
|
||||||
|
dst_addr: request.src_addr,
|
||||||
|
}) {
|
||||||
|
Ok(r) => r,
|
||||||
|
_ => {
|
||||||
|
log::error!("Internal tracker error: server response channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UdpRequest::Announce(announce) => {
|
||||||
|
log::trace!(
|
||||||
|
"(Server {}) Announce request: {:?}",
|
||||||
|
request.server_id,
|
||||||
|
announce
|
||||||
|
);
|
||||||
|
|
||||||
|
if !self.connection_valid(announce.connection_id, request.src_addr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we honour the desired number of peers, within our bounaries
|
||||||
|
|
||||||
|
let n_announce_want: u32 = if let Some(n) = announce.num_want {
|
||||||
|
if n < DEFAULT_ANNOUNCE_WANT {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
DEFAULT_ANNOUNCE_WANT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEFAULT_ANNOUNCE_WANT
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut n_announce_entries: u32 = 0;
|
||||||
|
let mut n_leechers: i32 = 0;
|
||||||
|
let mut n_seeders: i32 = 0;
|
||||||
|
let mut v4_peers: Vec<SocketAddrV4> = Vec::new();
|
||||||
|
let mut v6_peers: Vec<SocketAddrV6> = Vec::new();
|
||||||
|
|
||||||
|
let mut info_hashes = self.info_hashes.write().unwrap();
|
||||||
|
|
||||||
|
match info_hashes.get_mut(&announce.info_hash) {
|
||||||
|
Some(swarm) => {
|
||||||
|
if announce.event == Event::Stopped {
|
||||||
|
swarm.retain(|status| {
|
||||||
|
status.connection_id != announce.connection_id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let peers = self.peers.read().unwrap();
|
||||||
|
|
||||||
|
for peer_status in swarm {
|
||||||
|
if peer_status.last_event == Event::Stopped {
|
||||||
|
// Skip any peers who have stopped their download
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State update for ourselves
|
||||||
|
|
||||||
|
if peer_status.connection_id == announce.connection_id {
|
||||||
|
if peer_status.last_event != announce.event
|
||||||
|
&& announce.event != Event::None
|
||||||
|
{
|
||||||
|
// Update last event if the event has changed and is not just a `None` for notifying presence
|
||||||
|
peer_status.last_event = announce.event
|
||||||
|
} else {
|
||||||
|
// Peer status has not changed
|
||||||
|
peer_status.last_active = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
continue; // don't give the peer their own connection ID's address
|
||||||
|
}
|
||||||
|
|
||||||
|
if n_announce_entries >= n_announce_want {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect and add the relevant peer's address to the appropriate vector
|
||||||
|
|
||||||
|
if let Some(a) = peers.get(&peer_status.connection_id) {
|
||||||
|
if a.socket_addr == request.src_addr {
|
||||||
|
continue; // don't give the peer an expired ID with the same address
|
||||||
|
}
|
||||||
|
|
||||||
|
match &a.socket_addr {
|
||||||
|
SocketAddr::V4(v4) => {
|
||||||
|
if request.src_addr.is_ipv4() {
|
||||||
|
v4_peers.push(*v4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketAddr::V6(v6) => {
|
||||||
|
if request.src_addr.is_ipv6() {
|
||||||
|
v6_peers.push(*v6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep track of the seeders, leechers & announce entries gathered
|
||||||
|
|
||||||
|
if peer_status.last_event == Event::Completed {
|
||||||
|
n_seeders += 1;
|
||||||
|
} else {
|
||||||
|
n_leechers += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
n_announce_entries += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Info hash isn't currently tracked
|
||||||
|
// No relevant peers in the swarm
|
||||||
|
|
||||||
|
info_hashes.insert(
|
||||||
|
announce.info_hash,
|
||||||
|
vec![PeerStatus {
|
||||||
|
connection_id: announce.connection_id,
|
||||||
|
last_event: announce.event,
|
||||||
|
last_active: Instant::now(),
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp_common = AnnounceResponseCommon {
|
||||||
|
transaction_id: announce.transaction_id,
|
||||||
|
interval: DEFAULT_ANNOUNCE_INTERVAL.as_secs() as i32,
|
||||||
|
leechers: n_leechers,
|
||||||
|
seeders: n_seeders,
|
||||||
|
};
|
||||||
|
let response = if request.src_addr.is_ipv4() {
|
||||||
|
// IPv4
|
||||||
|
AnnounceResponse::V4(AnnounceResponseV4 {
|
||||||
|
inner: resp_common,
|
||||||
|
peers: v4_peers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// IPv6
|
||||||
|
AnnounceResponse::V6(AnnounceResponseV6 {
|
||||||
|
inner: resp_common,
|
||||||
|
peers: v6_peers,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.response_channels[request.server_id as usize].send(ResponseMessage {
|
||||||
|
response: UdpResponse::Announce(response),
|
||||||
|
dst_addr: request.src_addr,
|
||||||
|
}) {
|
||||||
|
Ok(r) => r,
|
||||||
|
_ => {
|
||||||
|
log::error!("Internal tracker error: server response channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UdpRequest::Scrape(scrape) => {
|
||||||
|
log::trace!(
|
||||||
|
"(Server {}) Scrape request: {:?}",
|
||||||
|
request.server_id,
|
||||||
|
scrape
|
||||||
|
);
|
||||||
|
|
||||||
|
if !self.connection_valid(scrape.connection_id, request.src_addr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let info_hashes = self.info_hashes.read().unwrap();
|
||||||
|
|
||||||
|
let mut scrape_stats: Vec<ScrapeStats> = Vec::new();
|
||||||
|
for info_hash in &scrape.info_hashes {
|
||||||
|
match info_hashes.get(info_hash) {
|
||||||
|
Some(peers) => {
|
||||||
|
scrape_stats.push(ScrapeStats {
|
||||||
|
seeders: peers
|
||||||
|
.iter()
|
||||||
|
.filter(|&peer_status| {
|
||||||
|
peer_status.last_event == Event::Completed
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
as i32,
|
||||||
|
completed: 0, // TODO: keep track of this
|
||||||
|
leechers: peers
|
||||||
|
.iter()
|
||||||
|
.filter(|&peer_status| {
|
||||||
|
peer_status.last_event == Event::Started
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
as i32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => scrape_stats.push(ScrapeStats::default()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.response_channels[request.server_id as usize].send(ResponseMessage {
|
||||||
|
response: UdpResponse::Scrape(ScrapeResponse {
|
||||||
|
transaction_id: scrape.transaction_id,
|
||||||
|
stats: scrape_stats,
|
||||||
|
}),
|
||||||
|
dst_addr: request.src_addr,
|
||||||
|
}) {
|
||||||
|
Ok(r) => r,
|
||||||
|
_ => {
|
||||||
|
log::error!("Internal tracker error: server response channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.server_threads.drain(..).for_each(|t| {
|
||||||
|
let _ = t.join();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_valid(&self, connection_id: i64, src_addr: SocketAddr) -> bool {
|
||||||
|
let peers = self.peers.read().unwrap();
|
||||||
|
|
||||||
|
return match peers.get(&connection_id) {
|
||||||
|
Some(metadata) => metadata.socket_addr == src_addr,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn purge_expired_connections(peers: &mut ConnectionIdMap) -> Vec<i64> {
|
||||||
|
let mut purged: Vec<i64> = Vec::new();
|
||||||
|
|
||||||
|
for (id, metadata) in peers.iter() {
|
||||||
|
if metadata.last_active.elapsed() > CONNECTION_EXPIRE_TIME {
|
||||||
|
purged.push(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in &purged {
|
||||||
|
peers.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return purged;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn purge_expired_swarm_peers(info_hashes: &mut InfoHashMap, expired_connections: &[i64]) {
|
||||||
|
info_hashes.retain(|_, swarm| {
|
||||||
|
swarm.is_empty()
|
||||||
|
|| swarm
|
||||||
|
.iter()
|
||||||
|
.find(|status| {
|
||||||
|
expired_connections.contains(&status.connection_id)
|
||||||
|
|| status.last_active.elapsed() > DEFAULT_ANNOUNCE_INTERVAL * 2
|
||||||
|
|| status.last_event == Event::Stopped
|
||||||
|
})
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_stop() -> bool {
|
||||||
|
SHOULD_STOP_SIGNAL.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
257
src/tracker/udp_server.rs
Normal file
257
src/tracker/udp_server.rs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
use std::net::{Ipv4Addr, SocketAddr, UdpSocket};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use log;
|
||||||
|
|
||||||
|
use crate::tracker::Tracker;
|
||||||
|
use crate::tracker::bittorrent::info_hash::InfoHash;
|
||||||
|
use crate::tracker::bittorrent::peer::PeerId;
|
||||||
|
use crate::tracker::bittorrent::protocol::Event;
|
||||||
|
use crate::tracker::bittorrent::udp::{
|
||||||
|
AnnounceRequest, AnnounceResponse, MIN_ANNOUNCE_REQUEST_SIZE, MIN_SCRAPE_REQUEST_SIZE,
|
||||||
|
ScrapeRequest, UdpResponse,
|
||||||
|
};
|
||||||
|
use crate::tracker::{
|
||||||
|
bittorrent::{
|
||||||
|
protocol::{Action, UDP_MAGIC},
|
||||||
|
udp::{ConnectRequest, UdpRequest},
|
||||||
|
},
|
||||||
|
tracker::{RequestMessage, ResponseMessage},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UdpServer {
|
||||||
|
addr: SocketAddr,
|
||||||
|
server_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const UDP_RECV_BUF_SIZE: usize = u16::MAX as usize;
|
||||||
|
const UDP_SEND_BUF_SIZE: usize = u16::MAX as usize; // Could also be 1500 for typical Ethernet MTU (?)
|
||||||
|
|
||||||
|
impl UdpServer {
|
||||||
|
pub fn new(addr: SocketAddr, id: u32) -> Self {
|
||||||
|
UdpServer {
|
||||||
|
addr: addr,
|
||||||
|
server_id: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(
|
||||||
|
&self,
|
||||||
|
req_chan: Sender<RequestMessage>,
|
||||||
|
resp_chan: Receiver<ResponseMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let recv_socket = UdpSocket::bind(self.addr)?;
|
||||||
|
let send_socket = recv_socket.try_clone()?;
|
||||||
|
let local_addr = recv_socket.local_addr()?;
|
||||||
|
let id = self.server_id;
|
||||||
|
|
||||||
|
log::info!("Starting UDP server on: {:?}", local_addr);
|
||||||
|
|
||||||
|
let recv_thread = thread::spawn(move || {
|
||||||
|
let mut buf: [u8; UDP_RECV_BUF_SIZE] = [0; UDP_RECV_BUF_SIZE];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if Tracker::should_stop() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
match recv_socket.recv_from(&mut buf) {
|
||||||
|
Ok((n_bytes, src)) => {
|
||||||
|
log::trace!("(Server {}) Received {} bytes from {}", id, n_bytes, src);
|
||||||
|
|
||||||
|
match try_parse_packet(&buf[..n_bytes]) {
|
||||||
|
Some(req) => {
|
||||||
|
if req_chan
|
||||||
|
.send(RequestMessage {
|
||||||
|
request: req,
|
||||||
|
src_addr: src,
|
||||||
|
server_id: id,
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"Internal tracker error: server {} request channel closed",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("Failed to receive on socket: {}", error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Server {} receive thread shutting exiting ...", id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let send_thread = thread::spawn(move || {
|
||||||
|
let mut buf: [u8; UDP_SEND_BUF_SIZE] = [0; UDP_SEND_BUF_SIZE];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if Tracker::should_stop() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match resp_chan.recv() {
|
||||||
|
Ok(r) => r,
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Internal tracker error: server {} response channel closed",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::trace!("(Server {}) Tracker response", id);
|
||||||
|
|
||||||
|
let n_bytes = match response.response {
|
||||||
|
UdpResponse::Connect(connect) => connect.write_to_buf(&mut buf),
|
||||||
|
UdpResponse::Announce(announce) => match announce {
|
||||||
|
AnnounceResponse::V4(v4) => v4.write_to_buf(&mut buf),
|
||||||
|
AnnounceResponse::V6(v6) => v6.write_to_buf(&mut buf),
|
||||||
|
},
|
||||||
|
UdpResponse::Scrape(scrape) => scrape.write_to_buf(&mut buf),
|
||||||
|
UdpResponse::Error(_) => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let n_sent = match send_socket.send_to(&buf[..n_bytes], response.dst_addr) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Failed to send {} bytes to {}: {}",
|
||||||
|
n_bytes,
|
||||||
|
response.dst_addr,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::trace!(
|
||||||
|
"(Server {}) Sent {} bytes to {}: {:x?}",
|
||||||
|
id,
|
||||||
|
n_sent,
|
||||||
|
response.dst_addr,
|
||||||
|
&buf[..n_bytes]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Server {} send thread shutting exiting ...", id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = recv_thread.join();
|
||||||
|
let _ = send_thread.join();
|
||||||
|
|
||||||
|
log::info!("Stopped UDP server on: {:?}", local_addr);
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_packet(buf: &[u8]) -> Option<UdpRequest> {
|
||||||
|
if buf.len() < 16 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let action: Action = Action::from_i32(i32::from_be_bytes(buf[8..12].try_into().ok()?))?;
|
||||||
|
|
||||||
|
let request = match action {
|
||||||
|
Action::Connect => UdpRequest::Connect(try_parse_connect(&buf)?),
|
||||||
|
Action::Announce => UdpRequest::Announce(try_parse_announce(&buf)?),
|
||||||
|
Action::Scrape => UdpRequest::Scrape(try_parse_scrape(&buf)?),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_connect(buf: &[u8]) -> Option<ConnectRequest> {
|
||||||
|
// Buffer length is checked to be at least 16 in `try_parse_packet`
|
||||||
|
|
||||||
|
let protocol_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?);
|
||||||
|
|
||||||
|
if protocol_id != UDP_MAGIC {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ConnectRequest {
|
||||||
|
protocol_id: protocol_id,
|
||||||
|
transaction_id: i32::from_be_bytes(buf[12..16].try_into().ok()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_announce(buf: &[u8]) -> Option<AnnounceRequest> {
|
||||||
|
if buf.len() < MIN_ANNOUNCE_REQUEST_SIZE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?);
|
||||||
|
let transaction_id: i32 = i32::from_be_bytes(buf[12..16].try_into().ok()?);
|
||||||
|
let info_hash: InfoHash = InfoHash {
|
||||||
|
bytes: buf[16..36].try_into().ok()?,
|
||||||
|
};
|
||||||
|
let peer_id: PeerId = PeerId {
|
||||||
|
bytes: buf[36..56].try_into().unwrap_or_default(),
|
||||||
|
};
|
||||||
|
let downloaded: i64 = i64::from_be_bytes(buf[56..64].try_into().ok()?);
|
||||||
|
let left: i64 = i64::from_be_bytes(buf[64..72].try_into().ok()?);
|
||||||
|
let uploaded: i64 = i64::from_be_bytes(buf[72..80].try_into().ok()?);
|
||||||
|
let event: Event = Event::from_i32(i32::from_be_bytes(buf[80..84].try_into().ok()?))?;
|
||||||
|
let ip_addr: Option<Ipv4Addr> = if buf[84..88].iter().all(|b| *b == 0x00) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Ipv4Addr::from_bits(u32::from_be_bytes(
|
||||||
|
buf[84..88].try_into().ok()?,
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
let key: u32 = u32::from_be_bytes(buf[88..92].try_into().ok()?);
|
||||||
|
let num_want: Option<u32> = {
|
||||||
|
let want = i32::from_be_bytes(buf[92..96].try_into().ok()?);
|
||||||
|
if want < 0 { None } else { Some(want as u32) }
|
||||||
|
};
|
||||||
|
let port: u16 = u16::from_be_bytes(buf[96..98].try_into().ok()?);
|
||||||
|
|
||||||
|
Some(AnnounceRequest {
|
||||||
|
connection_id: connection_id,
|
||||||
|
transaction_id: transaction_id,
|
||||||
|
info_hash: info_hash,
|
||||||
|
peer_id: peer_id,
|
||||||
|
downloaded: downloaded,
|
||||||
|
left: left,
|
||||||
|
uploaded: uploaded,
|
||||||
|
event: event,
|
||||||
|
ipv4_address: ip_addr,
|
||||||
|
key: key,
|
||||||
|
num_want: num_want,
|
||||||
|
port: port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_parse_scrape(buf: &[u8]) -> Option<ScrapeRequest> {
|
||||||
|
if buf.len() < MIN_SCRAPE_REQUEST_SIZE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection_id: i64 = i64::from_be_bytes(buf[0..8].try_into().ok()?);
|
||||||
|
let transaction_id: i32 = i32::from_be_bytes(buf[12..16].try_into().ok()?);
|
||||||
|
let info_hashes: Vec<InfoHash> = buf[16..]
|
||||||
|
.chunks_exact(20)
|
||||||
|
.map(|b| InfoHash {
|
||||||
|
bytes: b.try_into().unwrap(), // unwrap: `chunks_exact` guarantees the size
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(ScrapeRequest {
|
||||||
|
connection_id: connection_id,
|
||||||
|
transaction_id: transaction_id,
|
||||||
|
info_hashes: info_hashes,
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user