commit b806c63df4d317715725558e56b0f0a886be5af2 Author: eater <=@eater.me> Date: Sat Aug 29 12:22:05 2020 +0200 "Initial commit" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6d79cff --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1145 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "async-attributes" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43de69555a39d52918e2bc33a408d3c0a86c829b212d898f4ca25d21a6387478" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f47c78ea98277cb1f5e6f60ba4fc762f5eafe9f6511bc2f7dfd8b75c225650" +dependencies = [ + "async-io", + "futures-lite", + "multitask", + "parking 1.0.6", + "scoped-tls", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae22a338d28c75b53702b66f77979062cb29675db376d99e451af4fa79dedb3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "futures-lite", + "libc", + "once_cell", + "parking 2.0.0", + "polling", + "socket2", + "vec-arena", + "wepoll-sys-stjepang", + "winapi", +] + +[[package]] +name = "async-mutex" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e85981fc34e84cdff3fc2c9219189752633fdc538a06df8b5ac45b68a4f3a9" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c8da367da62b8ff2313c406c9ac091c1b31d67a165becdd2de380d846260f7" +dependencies = [ + "async-attributes", + "async-executor", + "async-io", + "async-mutex", + "async-task", + "blocking", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "backtrace" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "bendy" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f35e865a4aae9618063f67b1cd1ff6d0ae28b9478f1cdc0dd88d61884c8fe71" +dependencies = [ + "failure", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blocking" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5800d29218fea137b0880387e5948694a23c93fcdde157006966693a865c7c" +dependencies = [ + "async-channel", + "atomic-waker", + "futures-lite", + "once_cell", + "waker-fn", +] + +[[package]] +name = "bson" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11f16001d679cb13d14b2c93c7d0fa13bb484a87c34a6c4c39707ad936499b5" +dependencies = [ + "base64", + "chrono", + "hex", + "lazy_static", + "linked-hash-map", + "rand", + "serde", + "serde_json", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cc" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "event-listener" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd41440ae7e4734bbd42302f63eaba892afc93a3912dad84006247f0dedb0e" + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "fastrand" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd3bdaaf0a72155260a1c098989b60db1cbb22d6a628e64f16237aa4da93cc7" + +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" + +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97999970129b808f0ccba93211201d431fcc12d7e1ffae03a61b5cedd1a7ced2" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking 2.0.0", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" + +[[package]] +name = "futures-task" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" + +[[package]] +name = "hashbrown" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +dependencies = [ + "autocfg", +] + +[[package]] +name = "hermit-abi" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "js-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "miniz_oxide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" +dependencies = [ + "adler", +] + +[[package]] +name = "multitask" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09c35271e7dcdb5f709779111f2c8e8ab8e06c1b587c1c6a9e179d865aaa5b4" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + +[[package]] +name = "parking" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb300f271742d4a2a66c01b6b2fa0c83dfebd2e0bf11addb879a3547b4ed87c" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "polling" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e09dffb745feffca5be3dea51c02b7b368c4597ab0219a82acaf9799ab3e0d1" +dependencies = [ + "cfg-if", + "libc", + "wepoll-sys-stjepang", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" + +[[package]] +name = "proc-macro-hack" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "ring" +version = "0.16.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "serde" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117" + +[[package]] +name = "torment-bencode" +version = "0.1.0" +dependencies = [ + "bytes", +] + +[[package]] +name = "torment-cli" +version = "0.1.0" +dependencies = [ + "clap", + "torment-core", +] + +[[package]] +name = "torment-core" +version = "0.1.0" +dependencies = [ + "bendy", + "bytes", + "chrono", + "hex", + "lazy_static", + "num-traits", + "rand", + "ring", + "serde", + "serde_derive", + "torment-bencode", + "url", +] + +[[package]] +name = "torment-dht" +version = "0.1.0" +dependencies = [ + "bendy", + "rand", + "serde", + "serde_derive", + "torment-core", +] + +[[package]] +name = "torment-dht-node" +version = "0.1.0" +dependencies = [ + "async-std", + "bson", + "bytes", + "chrono", + "futures", + "rand", + "serde", + "serde_derive", + "torment-core", + "torment-dht", +] + +[[package]] +name = "torment-peer" +version = "0.1.0" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vec-arena" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb18268690309760d59ee1a9b21132c126ba384f374c59a94db4bc03adeb561" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "waker-fn" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9571542c2ce85ce642e6b58b3364da2fb53526360dfb7c211add4f5c23105ff7" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" + +[[package]] +name = "web-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-sys-stjepang" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd319e971980166b53e17b1026812ad66c6b54063be879eb182342b55284694" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5668755 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = [ + "torment-core", + "torment-cli", + "torment-dht", + "torment-dht-node", + "torment-bencode", + "torment-peer" +] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed0457d --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# eater's Torment + +A torrent client + +It's written in Rust, that's the only selling point really \ No newline at end of file diff --git a/torment-bencode/Cargo.toml b/torment-bencode/Cargo.toml new file mode 100644 index 0000000..4d367ac --- /dev/null +++ b/torment-bencode/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "torment-bencode" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytes = "0.5.6" \ No newline at end of file diff --git a/torment-bencode/src/lib.rs b/torment-bencode/src/lib.rs new file mode 100644 index 0000000..bfc4bef --- /dev/null +++ b/torment-bencode/src/lib.rs @@ -0,0 +1,284 @@ +#![allow(dead_code)] +use bytes::Bytes; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::num::ParseIntError; +use std::str::FromStr; +use std::string::FromUtf8Error; + +#[derive(Debug, Clone)] +pub enum BencodeValue { + Int(i64), + Bytes(Bytes), + Dict(BencodeDict), + List(BencodeList), +} + +#[derive(Debug)] +pub enum BencodeError { + InputTooShort, + UnknownType, + FailedToParseNumber(ParseIntError), + Expected(char), +} + +impl Display for BencodeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Error for BencodeError {} + +impl BencodeValue { + pub fn bytes(&self) -> Option { + if let BencodeValue::Bytes(bytes) = self { + Some(bytes.clone()) + } else { + None + } + } + + pub fn string(&self) -> Option> { + if let Some(bytes) = self.bytes() { + Some(String::from_utf8(bytes.to_vec())) + } else { + None + } + } + + pub fn int(&self) -> Option { + if let BencodeValue::Int(num) = self { + Some(*num) + } else { + None + } + } + + pub fn dict(&self) -> Option<&BencodeDict> { + if let BencodeValue::Dict(dict) = self { + Some(dict) + } else { + None + } + } + + pub fn list(&self) -> Option<&BencodeList> { + if let BencodeValue::List(list) = self { + Some(list) + } else { + None + } + } + + pub fn decode>(bytes: T) -> Result { + let bytes = bytes.into(); + Ok(Self::consume(bytes)?.1) + } + + fn consume(bytes: Bytes) -> Result<(usize, Self), BencodeError> { + if bytes.len() < 2 { + return Err(BencodeError::InputTooShort); + } + + match bytes[0] { + b'd' => { + let (consumed, item) = BencodeDict::consume(bytes)?; + Ok((consumed, BencodeValue::Dict(item))) + } + + b'0'..=b'9' => { + let (consumed, item) = BencodeValue::consume_bytes(bytes)?; + Ok((consumed, BencodeValue::Bytes(item))) + } + + b'i' => { + let (consumed, item) = BencodeValue::consume_int(bytes)?; + Ok((consumed, BencodeValue::Int(item))) + } + + b'l' => { + let (consumed, list) = BencodeList::consume(bytes)?; + Ok((consumed, BencodeValue::List(list))) + } + + _ => Err(BencodeError::UnknownType), + } + } + + fn consume_int(bytes: Bytes) -> Result<(usize, i64), BencodeError> { + let mut offset = 1; + while bytes.len() > offset && bytes[offset] != b'e' { + offset += 1; + } + + if bytes.len() <= offset { + return Err(BencodeError::Expected('e')); + } + + let length = String::from_utf8_lossy(&bytes[1..offset]); + offset += 1; + let nr = i64::from_str(&length) + .map_err(|parse_error| BencodeError::FailedToParseNumber(parse_error))?; + + Ok((offset, nr)) + } + + fn consume_bytes(bytes: Bytes) -> Result<(usize, Bytes), BencodeError> { + let mut offset = 1; + while bytes.len() > offset && bytes[offset] != b':' { + offset += 1; + } + + if bytes.len() <= offset { + return Err(BencodeError::Expected(':')); + } + let length = String::from_utf8_lossy(&bytes[0..offset]); + offset += 1; + let length = usize::from_str(&length) + .map_err(|parse_error| BencodeError::FailedToParseNumber(parse_error))?; + let result = bytes.slice(offset..offset + length); + Ok((offset + length, result)) + } +} + +#[derive(Debug, Clone)] +pub struct BencodeDict { + bytes: Bytes, + map: HashMap, +} + +impl BencodeDict { + pub fn buffer(&self) -> Bytes { + self.bytes.clone() + } + + fn consume(bytes: Bytes) -> Result<(usize, Self), BencodeError> { + let mut offset = 1; + let mut map = HashMap::new(); + while bytes[offset] != b'e' { + // consume key + let (len, str) = BencodeValue::consume_bytes(bytes.slice(offset..))?; + offset += len; + let (len, value) = BencodeValue::consume(bytes.slice(offset..))?; + offset += len; + + map.insert(str, value); + } + + offset += 1; + + Ok(( + offset, + BencodeDict { + bytes: bytes.slice(0..offset), + map, + }, + )) + } + + pub fn contains_key(&self, key: &[u8]) -> bool { + self.map.contains_key(key) + } + + pub fn get(&self, key: &[u8]) -> Option<&BencodeValue> { + self.map.get(key) + } +} + +#[derive(Debug, Clone)] +pub struct BencodeList { + bytes: Bytes, + list: Vec, +} + +impl BencodeList { + pub fn buffer(&self) -> Bytes { + self.bytes.clone() + } + + fn consume(bytes: Bytes) -> Result<(usize, Self), BencodeError> { + let mut offset = 1; + let mut list = vec![]; + + while bytes.len() > offset && bytes[offset] != b'e' { + let (consumed, item) = BencodeValue::consume(bytes.slice(offset..))?; + offset += consumed; + list.push(item); + } + + offset += 1; + + Ok(( + offset, + BencodeList { + bytes: bytes.slice(0..offset), + list, + }, + )) + } + + pub fn len(&self) -> usize { + self.list.len() + } + + pub fn iter(&self) -> impl Iterator { + self.list.iter() + } + + pub fn get(&self, index: usize) -> Option<&BencodeValue> { + self.list.get(index) + } +} + +#[cfg(test)] +mod tests { + use crate::BencodeValue; + + #[test] + fn test_string() { + let input = b"4:spam"; + let value = BencodeValue::decode(input.as_ref()).unwrap(); + if let BencodeValue::Bytes(bytes) = value { + assert_eq!(bytes.as_ref(), b"spam"); + } else { + panic!(":(") + } + } + + #[test] + fn test_integer() { + let input = b"i42e"; + let value = BencodeValue::decode(input.as_ref()).unwrap(); + if let BencodeValue::Int(nr) = value { + assert_eq!(nr, 42); + } else { + panic!(":(") + } + } + + #[test] + fn test_dict() { + let input = b"d4:spam3:egg3:eggdee"; + let value = BencodeValue::decode(input.as_ref()).unwrap(); + if let BencodeValue::Dict(dict) = value { + assert!(dict.contains_key(b"spam")); + assert_eq!(dict.get(b"spam").unwrap().bytes().unwrap().as_ref(), b"egg"); + } else { + panic!(":(") + } + } + + #[test] + fn test_list() { + let input = b"l4:spam3:egge"; + let value = BencodeValue::decode(input.as_ref()).unwrap(); + if let BencodeValue::List(list) = value { + assert_eq!(list.get(0).unwrap().bytes().unwrap().as_ref(), b"spam"); + assert_eq!(list.get(1).unwrap().bytes().unwrap().as_ref(), b"egg"); + } else { + panic!(":(") + } + } +} diff --git a/torment-cli/Cargo.toml b/torment-cli/Cargo.toml new file mode 100644 index 0000000..7fb298f --- /dev/null +++ b/torment-cli/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "torment-cli" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = "2.33.3" +torment-core = { path = "../torment-core" } \ No newline at end of file diff --git a/torment-cli/src/main.rs b/torment-cli/src/main.rs new file mode 100644 index 0000000..1367467 --- /dev/null +++ b/torment-cli/src/main.rs @@ -0,0 +1,39 @@ +use clap::{App, Arg, SubCommand}; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use torment_core::metainfo::Torrent; + +fn main() { + let matches = App::new("torment-cli") + .version(env!("CARGO_PKG_VERSION")) + .subcommand( + SubCommand::with_name("torrent") + .about("Shows details about torrent file") + .arg(Arg::with_name("file").required(true)), + ) + .get_matches(); + + match matches.subcommand() { + ("torrent", Some(subcmd)) => { + let file = subcmd.value_of("file").unwrap(); + let mut bytes: Vec = vec![]; + File::open(file).unwrap().read_to_end(&mut bytes).unwrap(); + match Torrent::from_bytes( + &bytes, + Path::new(file) + .file_stem() + .and_then(|file_name| file_name.to_str()) + .map(|str| str.to_string()), + ) { + Ok(torrent) => { + println!("{:#?}", torrent); + } + + Err(err) => println!("Error: {}", err), + } + } + + _ => {} + } +} diff --git a/torment-core/Cargo.toml b/torment-core/Cargo.toml new file mode 100644 index 0000000..7344596 --- /dev/null +++ b/torment-core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "torment-core" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +torment-bencode = { path = "../torment-bencode" } +hex = "0.4.2" +rand = "0.7.3" +bendy = "0.3.2" +serde = "1.0.115" +serde_derive = "1.0.115" +chrono = "0.4.15" +num-traits = "0.2.12" +url = "2.1.1" +bytes = "0.5.6" +ring = "0.16.15" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/torment-core/src/infohash/mod.rs b/torment-core/src/infohash/mod.rs new file mode 100644 index 0000000..7751f70 --- /dev/null +++ b/torment-core/src/infohash/mod.rs @@ -0,0 +1,218 @@ +pub mod v1; +pub mod v2; + +use hex::{FromHex, FromHexError, ToHex}; +use std::convert::TryInto; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::iter; +pub use v1::InfoHash as InfoHashV1; +pub use v2::InfoHash as InfoHashV2; + +pub trait InfoHashCapable: ToHex + FromHex { + fn version(&self) -> u32; + fn to_bytes(&self) -> Box<[u8]>; + fn to_bytes_truncated(&self) -> [u8; 20] { + self.to_bytes()[0..20].try_into().unwrap() + } + + /// Creates a V1 style info hash from this info hash + fn to_v1(&self) -> InfoHashV1; + fn from_bytes>(bytes: T) -> Result; +} + +#[derive(Debug)] +pub enum InfoHashFromHashError { + InvalidLength { + expected: Option, + actual: usize, + }, + HexEncodingError(FromHexError), +} + +impl Error for InfoHashFromHashError {} + +impl Display for InfoHashFromHashError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::HexEncodingError(error) => write!(f, "Input is invalid hex: {}", error), + Self::InvalidLength { expected, actual } => write!( + f, + "Input has invalid length of {} expected {}", + actual, + expected + .map(|x| format!("{}", x)) + .unwrap_or("40 or 64".to_string()) + ), + } + } +} + +#[derive(Debug)] +pub enum InfoHashFromBytesError { + InvalidLength { + expected: Option, + actual: usize, + }, +} + +impl Error for InfoHashFromBytesError {} + +impl Display for InfoHashFromBytesError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidLength { expected, actual } => write!( + f, + "Input has invalid length of {} expected {}", + actual, + expected + .map(|x| format!("{}", x)) + .unwrap_or("20 or 32".to_string()) + ), + } + } +} + +pub enum HybridInfoHash { + V1(InfoHashV1), + V2(InfoHashV2), +} + +impl Display for HybridInfoHash { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.encode_hex::().as_str()) + } +} + +impl ToHex for HybridInfoHash { + fn encode_hex>(&self) -> T { + match self { + Self::V1(info) => info.encode_hex(), + Self::V2(info) => info.encode_hex(), + } + } + + fn encode_hex_upper>(&self) -> T { + match self { + Self::V1(info) => info.encode_hex_upper(), + Self::V2(info) => info.encode_hex_upper(), + } + } +} + +impl FromHex for HybridInfoHash { + type Error = InfoHashFromHashError; + + fn from_hex>(hex: T) -> Result { + let hex = hex.as_ref(); + Ok(match hex.len() { + 64 => Self::V2(InfoHashV2::from_hex(hex)?), + 40 => Self::V1(InfoHashV1::from_hex(hex)?), + len => { + return Err(InfoHashFromHashError::InvalidLength { + actual: len, + expected: None, + }); + } + }) + } +} + +impl InfoHashCapable for HybridInfoHash { + fn version(&self) -> u32 { + match self { + Self::V1(_) => 1, + Self::V2(_) => 2, + } + } + + fn to_bytes(&self) -> Box<[u8]> { + match self { + Self::V1(info) => info.to_bytes(), + Self::V2(info) => info.to_bytes(), + } + } + + fn to_v1(&self) -> InfoHashV1 { + match self { + Self::V1(info) => info.to_v1(), + Self::V2(info) => info.to_v1(), + } + } + + fn from_bytes>(bytes: T) -> Result { + let bytes_ref = bytes.as_ref(); + match bytes_ref.len() { + 20 => Ok(Self::V1(InfoHashV1::from_bytes(bytes)?)), + 32 => Ok(Self::V2(InfoHashV2::from_bytes(bytes)?)), + len => Err(InfoHashFromBytesError::InvalidLength { + expected: None, + actual: len, + }), + } + } +} + +#[cfg(test)] +mod test { + use crate::infohash::{HybridInfoHash, InfoHashCapable}; + use hex::{FromHex, ToHex}; + + #[test] + fn conversion() { + let info_hash_bytes: Vec = vec![ + 0x0a, 0xfe, 0x96, 0xa8, 0x62, 0x37, 0x8e, 0x7f, 0x69, 0x9d, 0x54, 0x6b, 0x7b, 0x8f, + 0xe6, 0xfc, 0x47, 0xd0, 0xe2, 0x4a, + ]; + let info_hash = + HybridInfoHash::from_bytes(&info_hash_bytes).expect("Failed to parse info hash (v1)"); + assert_eq!(1, info_hash.version()); + assert_eq!(info_hash_bytes, info_hash.to_bytes().to_vec(), "Failed to assert that info hash (v1) could be converted to byte format and back without changing"); + + let info_hash_bytes: Vec = vec![ + 0xa3, 0x1f, 0xe9, 0x65, 0x6f, 0xc8, 0xd3, 0xa4, 0x59, 0xe6, 0x23, 0xdc, 0x82, 0x04, + 0xe6, 0xd0, 0x26, 0x8f, 0x8d, 0xf5, 0x6d, 0x73, 0x4d, 0xac, 0x3c, 0xa3, 0x26, 0x2e, + 0xdb, 0x5d, 0xb8, 0x83, + ]; + let info_hash = + HybridInfoHash::from_bytes(&info_hash_bytes).expect("Failed to parse info hash (v2)"); + assert_eq!(info_hash_bytes, info_hash.to_bytes().to_vec(), "Failed to assert that info hash (v2) could be converted to byte format and back without changing"); + } + + #[test] + fn hex_conversion() { + let info_hash_hex = "0afe96a862378e7f699d546b7b8fe6fc47d0e24a"; + let info_hash = + HybridInfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash (v1)"); + assert_eq!(1, info_hash.version()); + assert_eq!(info_hash_hex, info_hash.encode_hex::().as_str(), "Failed to assert that info hash (v1) could be converted to hex format and back without changing"); + + let info_hash_hex = "a31fe9656fc8d3a459e623dc8204e6d0268f8df56d734dac3ca3262edb5db883"; + let info_hash = + HybridInfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash (v2)"); + assert_eq!(2, info_hash.version()); + assert_eq!(info_hash_hex, info_hash.encode_hex::().as_str(), "Failed to assert that info hash (v2) could be converted to hex format and back without changing"); + } + + #[test] + fn truncation() { + let info_hash_hex = "0afe96a862378e7f699d546b7b8fe6fc47d0e24a"; + let info_hash = + HybridInfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash (v1)"); + assert_eq!(1, info_hash.version()); + assert_eq!( + info_hash_hex, + info_hash.to_v1().encode_hex::().as_str(), + "Failed to assert that info hash (v1) could be truncated to v1 hash without changing" + ); + + let info_hash_hex = "a31fe9656fc8d3a459e623dc8204e6d0268f8df56d734dac3ca3262edb5db883"; + let info_hash = + HybridInfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash (v2)"); + assert_eq!( + &info_hash_hex[0..40], + info_hash.to_v1().encode_hex::().as_str(), + "Failed to assert that info hash (v2) could be truncated to v1 hash without changing" + ); + } +} diff --git a/torment-core/src/infohash/v1.rs b/torment-core/src/infohash/v1.rs new file mode 100644 index 0000000..a766945 --- /dev/null +++ b/torment-core/src/infohash/v1.rs @@ -0,0 +1,354 @@ +use crate::infohash::{InfoHashCapable, InfoHashFromBytesError, InfoHashFromHashError, InfoHashV1}; +use bendy::decoding::{Error as DecodingError, FromBencode, Object}; +use bendy::encoding::{Error as EncodingError, SingleItemEncoder, ToBencode}; +use hex::{FromHex, ToHex}; +use rand::random; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::convert::{TryFrom, TryInto}; +use std::fmt::{Debug, Display, Formatter}; +use std::iter; +use std::ops::{Add, BitAnd, BitOr, BitXor, Rem, Sub}; + +/// A make shift u160 type which uses a single u128 and u32 to keep operations speedy +/// Only really meant for ordering, bit operations and indexing +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Hash, Default)] +pub struct U160(pub(crate) u128, pub(crate) u32); + +impl Serialize for U160 { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.collect_str(&self) + } +} + +impl<'de> Deserialize<'de> for U160 { + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + String::deserialize(deserializer) + .and_then(|x| U160::from_hex(x).map_err(|err| D::Error::custom(err.to_string()))) + } +} + +impl U160 { + pub const MAX: U160 = U160(u128::MAX, u32::MAX); + pub const MIN: U160 = U160(u128::MIN, u32::MIN); + pub const ONE: U160 = U160(0, 1); + pub const ZERO: U160 = U160(0, 0); + + pub fn leading_zeroes(&self) -> u8 { + let mut leading_zeroes = self.0.leading_zeros() as u8; + if leading_zeroes == 128 { + leading_zeroes += self.1.leading_zeros() as u8; + } + + leading_zeroes + } +} + +impl ToBencode for U160 { + const MAX_DEPTH: usize = 0; + + fn encode(&self, encoder: SingleItemEncoder) -> Result<(), EncodingError> { + encoder.emit_bytes(&self.to_byte_array()[..]) + } +} + +impl FromBencode for U160 { + fn decode_bencode_object(object: Object) -> Result + where + Self: Sized, + { + U160::from_bytes(object.try_into_bytes()?).map_err(|x| DecodingError::malformed_content(x)) + } +} + +impl<'obj, 'ser> TryFrom> for U160 { + type Error = DecodingError; + + fn try_from(value: Object<'obj, 'ser>) -> Result { + U160::decode_bencode_object(value) + } +} + +impl Add for U160 { + type Output = U160; + + fn add(self, rhs: Self) -> Self::Output { + let (lower, overflow) = self.1.overflowing_add(rhs.1); + U160(overflow as u128 + self.0 + rhs.0, lower) + } +} + +impl Sub for U160 { + type Output = U160; + + fn sub(self, rhs: Self) -> Self::Output { + let (lower, overflow) = self.1.overflowing_sub(rhs.1); + U160((self.0 - rhs.0) - overflow as u128, lower) + } +} + +impl U160 { + pub fn random() -> U160 { + U160(random(), random()) + } + + pub fn half(&self) -> U160 { + U160( + self.0 / 2, + (self.1 / 2) + + if self.0.rem(2) == 1 { + 0b10000000_00000000_00000000_00000000 + } else { + 0 + }, + ) + } +} + +pub type InfoHash = U160; + +impl Debug for U160 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !f.alternate() { + f.write_str(&self.encode_hex::()) + } else { + f.debug_tuple("U160").field(&self.0).field(&self.1).finish() + } + } +} + +impl Display for U160 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.encode_hex::().as_str()) + } +} + +impl From<&[u8; 20]> for InfoHash { + fn from(input: &[u8; 20]) -> Self { + U160( + u128::from_be_bytes(input[0..16].try_into().unwrap()), + u32::from_be_bytes(input[16..20].try_into().unwrap()), + ) + } +} + +impl From<[u8; 20]> for InfoHash { + fn from(input: [u8; 20]) -> Self { + Self::from(&input) + } +} + +impl InfoHashCapable for InfoHash { + fn version(&self) -> u32 { + 1 + } + + fn to_bytes(&self) -> Box<[u8]> { + self.to_byte_array().into() + } + + fn to_v1(&self) -> InfoHashV1 { + *self + } + + fn from_bytes>(bytes: T) -> Result { + let bytes_ref = bytes.as_ref(); + + if bytes_ref.len() != 20 { + return Err(InfoHashFromBytesError::InvalidLength { + actual: bytes_ref.len(), + expected: Some(20), + }); + } + + let byte_array: &[u8; 20] = bytes_ref.try_into().unwrap(); + Ok(Self::from(byte_array)) + } +} + +impl InfoHash { + pub fn to_byte_array(&self) -> [u8; 20] { + let mut bytes = [0u8; 20]; + bytes[0..16].copy_from_slice(&self.0.to_be_bytes()); + bytes[16..].copy_from_slice(&self.1.to_be_bytes()); + bytes + } +} + +impl FromHex for InfoHash { + type Error = InfoHashFromHashError; + + fn from_hex>(hex: T) -> Result { + let hex_ref = hex.as_ref(); + if hex_ref.len() != 40 { + return Err(InfoHashFromHashError::InvalidLength { + expected: Some(40), + actual: hex.as_ref().len(), + }); + } + + let decoded = hex::decode(hex_ref).map_err(InfoHashFromHashError::HexEncodingError)?; + Ok(U160::from_bytes(decoded).unwrap()) + } +} + +impl ToHex for InfoHash { + fn encode_hex>(&self) -> T { + self.to_byte_array().encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + self.to_byte_array().encode_hex_upper() + } +} + +impl BitXor for U160 { + type Output = U160; + + fn bitxor(self, rhs: Self) -> Self::Output { + U160(self.0 ^ rhs.0, self.1 ^ rhs.1) + } +} + +impl BitAnd for U160 { + type Output = U160; + + fn bitand(self, rhs: Self) -> Self::Output { + U160(self.0 & rhs.0, self.1 & rhs.1) + } +} + +impl BitOr for U160 { + type Output = U160; + + fn bitor(self, rhs: Self) -> Self::Output { + U160(self.0 | rhs.0, self.1 | rhs.1) + } +} + +#[cfg(test)] +mod test { + use crate::infohash::v1::{InfoHash, U160}; + use crate::infohash::{InfoHashCapable, InfoHashFromBytesError, InfoHashFromHashError}; + use hex::{FromHex, ToHex}; + + #[test] + fn conversion() { + let info_hash_bytes: [u8; 20] = [ + 0x0a, 0xfe, 0x96, 0xa8, 0x62, 0x37, 0x8e, 0x7f, 0x69, 0x9d, 0x54, 0x6b, 0x7b, 0x8f, + 0xe6, 0xfc, 0x47, 0xd0, 0xe2, 0x4a, + ]; + let info_hash = InfoHash::from_bytes(info_hash_bytes).expect("Failed to parse info hash"); + assert_eq!(1, info_hash.version()); + assert_eq!(info_hash_bytes, info_hash.to_byte_array(), "Failed to assert that info hash could be converted to byte format and back without changing") + } + + #[test] + fn hex_conversion() { + let info_hash_hex = "0afe96a862378e7f699d546b7b8fe6fc47d0e24a"; + let info_hash = InfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash"); + assert_eq!(info_hash_hex, info_hash.encode_hex::().as_str(), "Failed to assert that info hash could be converted to hex format and back without changing") + } + + #[test] + fn formatting() { + let info_hash_hex = "0afe96a862378e7f699d546b7b8fe6fc47d0e24a"; + let info_hash = InfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash"); + assert_eq!( + "0afe96a862378e7f699d546b7b8fe6fc47d0e24a", + format!("{}", info_hash) + ); + + assert_eq!( + "0afe96a862378e7f699d546b7b8fe6fc47d0e24a", + format!("{:?}", info_hash) + ); + + assert_eq!( + "U160( + 14614179062085549902615142429809960700, + 1204871754, +)", + format!("{:#?}", info_hash) + ); + } + + #[test] + fn bit_operations() { + let info_hash_alpha: InfoHash = [0u8; 20].into(); + let info_hash_omega: InfoHash = [255u8; 20].into(); + assert_eq!( + info_hash_omega ^ info_hash_omega, + info_hash_alpha, + "XOR has invalid result" + ); + + assert_eq!( + info_hash_alpha ^ info_hash_omega, + info_hash_omega, + "XOR has invalid result" + ); + + assert_eq!( + info_hash_alpha | info_hash_omega, + info_hash_omega, + "OR has invalid result" + ); + + assert_eq!( + info_hash_alpha | info_hash_alpha, + info_hash_alpha, + "OR has invalid result" + ); + + assert_eq!( + info_hash_alpha & info_hash_omega, + info_hash_alpha, + "OR has invalid result" + ); + + assert_eq!( + info_hash_omega & info_hash_omega, + info_hash_omega, + "OR has invalid result" + ); + } + + #[test] + fn ordering() { + assert!(U160(1, 0) > U160(0, u32::MAX)); + assert!(U160(0, 0) < U160(0, u32::MAX)); + } + + #[test] + fn invalid_length() { + let info_hash_hex = "0afe96a862378e7f699d546b7b8fe6fc47d0e24a3"; + let info_hash = InfoHash::from_hex(info_hash_hex).expect_err("No error thrown"); + + if let InfoHashFromHashError::InvalidLength { expected, actual } = info_hash { + assert_eq!(expected, Some(40)); + assert_eq!(actual, 41); + } else { + panic!("Should've thrown invalid length"); + } + + let info_hash_hex = hex::decode("0afe96a862378e7f699d546b7b8fe6fc47d0e24aaa").unwrap(); + let info_hash = InfoHash::from_bytes(info_hash_hex).expect_err("No error thrown"); + + let InfoHashFromBytesError::InvalidLength { expected, actual } = info_hash; + assert_eq!(expected, Some(20)); + assert_eq!(actual, 21); + } + + #[test] + fn u160_math() { + assert_eq!(U160::ZERO + U160::ONE, U160::ONE); + assert_eq!(U160(1, 0) - U160::ONE, U160(0, u32::MAX)) + } +} diff --git a/torment-core/src/infohash/v2.rs b/torment-core/src/infohash/v2.rs new file mode 100644 index 0000000..d3a0f64 --- /dev/null +++ b/torment-core/src/infohash/v2.rs @@ -0,0 +1,212 @@ +use crate::infohash::v1::U160; +use crate::infohash::{InfoHashCapable, InfoHashFromBytesError, InfoHashFromHashError, InfoHashV1}; +use hex::{FromHex, ToHex}; +use rand::random; +use std::convert::TryInto; +use std::fmt::{Debug, Display, Formatter}; +use std::iter; +use std::ops::{BitAnd, BitOr, BitXor}; + +/// A make shift u265 type which uses a 2 u128's to keep operations speedy +/// Only really meant for ordering, bit operations and indexing so no math ops are implemented +#[derive(PartialOrd, Ord, Eq, PartialEq, Copy, Clone, Hash)] +pub struct U256(pub(crate) u128, pub(crate) u128); + +impl U256 { + pub fn random() -> U256 { + U256(random(), random()) + } +} + +pub type InfoHash = U256; + +impl Debug for U256 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !f.alternate() { + f.write_str(&self.encode_hex::()) + } else { + f.debug_tuple("U256").field(&self.0).field(&self.1).finish() + } + } +} + +impl Display for U256 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.encode_hex::().as_str()) + } +} + +impl From<&[u8; 32]> for InfoHash { + fn from(input: &[u8; 32]) -> Self { + U256( + u128::from_be_bytes(input[0..16].try_into().unwrap()), + u128::from_be_bytes(input[16..32].try_into().unwrap()), + ) + } +} + +impl From<[u8; 32]> for InfoHash { + fn from(input: [u8; 32]) -> Self { + Self::from(&input) + } +} + +impl InfoHashCapable for InfoHash { + fn version(&self) -> u32 { + 2 + } + + fn to_bytes(&self) -> Box<[u8]> { + self.to_byte_array().into() + } + + fn to_v1(&self) -> InfoHashV1 { + U160(self.0, (self.1 >> 96) as u32) + } + + fn from_bytes>(bytes: T) -> Result { + let bytes_ref = bytes.as_ref(); + + if bytes_ref.len() != 32 { + return Err(InfoHashFromBytesError::InvalidLength { + actual: bytes_ref.len(), + expected: Some(32), + }); + } + + let byte_array: &[u8; 32] = bytes_ref.try_into().unwrap(); + Ok(Self::from(byte_array)) + } +} + +impl InfoHash { + pub fn to_byte_array(&self) -> [u8; 32] { + let mut bytes = [0u8; 32]; + bytes[0..16].copy_from_slice(&self.0.to_be_bytes()); + bytes[16..].copy_from_slice(&self.1.to_be_bytes()); + bytes + } +} + +impl FromHex for InfoHash { + type Error = InfoHashFromHashError; + + fn from_hex>(hex: T) -> Result { + let hex_ref = hex.as_ref(); + if hex_ref.len() != 64 { + return Err(InfoHashFromHashError::InvalidLength { + expected: Some(64), + actual: hex.as_ref().len(), + }); + } + + let decoded = hex::decode(hex_ref).map_err(InfoHashFromHashError::HexEncodingError)?; + Ok(U256::from_bytes(decoded).unwrap()) + } +} + +impl ToHex for U256 { + fn encode_hex>(&self) -> T { + self.to_byte_array().encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + self.to_byte_array().encode_hex_upper() + } +} + +impl BitXor for U256 { + type Output = U256; + + fn bitxor(self, rhs: Self) -> Self::Output { + U256(self.0 ^ rhs.0, self.1 ^ rhs.1) + } +} + +impl BitAnd for U256 { + type Output = U256; + + fn bitand(self, rhs: Self) -> Self::Output { + U256(self.0 & rhs.0, self.1 & rhs.1) + } +} + +impl BitOr for U256 { + type Output = U256; + + fn bitor(self, rhs: Self) -> Self::Output { + U256(self.0 | rhs.0, self.1 | rhs.1) + } +} + +#[cfg(test)] +mod test { + use crate::infohash::v2::{InfoHash, U256}; + use crate::infohash::InfoHashCapable; + use hex::{FromHex, ToHex}; + + #[test] + fn conversion() { + let info_hash_bytes: [u8; 32] = [ + 0xa3, 0x1f, 0xe9, 0x65, 0x6f, 0xc8, 0xd3, 0xa4, 0x59, 0xe6, 0x23, 0xdc, 0x82, 0x04, + 0xe6, 0xd0, 0x26, 0x8f, 0x8d, 0xf5, 0x6d, 0x73, 0x4d, 0xac, 0x3c, 0xa3, 0x26, 0x2e, + 0xdb, 0x5d, 0xb8, 0x83, + ]; + let info_hash = InfoHash::from_bytes(info_hash_bytes).expect("Failed to parse info hash"); + assert_eq!(info_hash_bytes, info_hash.to_byte_array(), "Failed to assert that info hash could be converted to byte format and back without changing") + } + + #[test] + fn hex_conversion() { + let info_hash_hex = "a31fe9656fc8d3a459e623dc8204e6d0268f8df56d734dac3ca3262edb5db883"; + let info_hash = InfoHash::from_hex(info_hash_hex).expect("Failed to parse info hash"); + assert_eq!(info_hash_hex, info_hash.encode_hex::().as_str(), "Failed to assert that info hash could be converted to hex format and back without changing") + } + + #[test] + fn bit_operations() { + let info_hash_alpha: InfoHash = [0u8; 32].into(); + let info_hash_omega: InfoHash = [255u8; 32].into(); + assert_eq!( + info_hash_omega ^ info_hash_omega, + info_hash_alpha, + "XOR has invalid result" + ); + + assert_eq!( + info_hash_alpha ^ info_hash_omega, + info_hash_omega, + "XOR has invalid result" + ); + + assert_eq!( + info_hash_alpha | info_hash_omega, + info_hash_omega, + "OR has invalid result" + ); + + assert_eq!( + info_hash_alpha | info_hash_alpha, + info_hash_alpha, + "OR has invalid result" + ); + + assert_eq!( + info_hash_alpha & info_hash_omega, + info_hash_alpha, + "OR has invalid result" + ); + + assert_eq!( + info_hash_omega & info_hash_omega, + info_hash_omega, + "OR has invalid result" + ); + } + + #[test] + fn ordering() { + assert!(U256(1, 0) > U256(0, u128::MAX)); + assert!(U256(0, 0) < U256(0, u128::MAX)); + } +} diff --git a/torment-core/src/ip.rs b/torment-core/src/ip.rs new file mode 100644 index 0000000..77c5417 --- /dev/null +++ b/torment-core/src/ip.rs @@ -0,0 +1,580 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +/// This code is the unstable ip code from the rust std +/// Since I would like to build on the stable rust, I just copied it yolo + +#[derive(Copy, PartialEq, Eq, Clone, Hash, Debug)] +pub enum Ipv6MulticastScope { + InterfaceLocal, + LinkLocal, + RealmLocal, + AdminLocal, + SiteLocal, + OrganizationLocal, + Global, +} + +pub trait IpAddrExt { + fn ext_is_global(&self) -> bool; + fn ext_is_documentation(&self) -> bool; +} + +impl IpAddrExt for IpAddr { + /// Returns [`true`] if the address appears to be globally routable. + /// + /// See the documentation for [`Ipv4Addr::is_global`][IPv4] and + /// [`Ipv6Addr::is_global`][IPv6] for more details. + /// + /// [IPv4]: ../../std/net/struct.Ipv4Addr.html#method.is_global + /// [IPv6]: ../../std/net/struct.Ipv6Addr.html#method.is_global + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// use torment_core::ip::IpAddrExt; + /// + /// assert_eq!(IpAddr::V4(Ipv4Addr::new(80, 9, 12, 3)).ext_is_global(), true); + /// assert_eq!(IpAddr::V6(Ipv6Addr::new(0, 0, 0x1c9, 0, 0, 0xafc8, 0, 0x1)).ext_is_global(), true); + /// ``` + fn ext_is_global(&self) -> bool { + match self { + IpAddr::V4(ip) => ip.ext_is_global(), + IpAddr::V6(ip) => ip.ext_is_global(), + } + } + + /// Returns [`true`] if this address is in a range designated for documentation. + /// + /// See the documentation for [`Ipv4Addr::is_documentation`][IPv4] and + /// [`Ipv6Addr::is_documentation`][IPv6] for more details. + /// + /// [IPv4]: ../../std/net/struct.Ipv4Addr.html#method.is_documentation + /// [IPv6]: ../../std/net/struct.Ipv6Addr.html#method.is_documentation + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + /// use torment_core::ip::IpAddrExt; + /// + /// assert_eq!(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 6)).ext_is_documentation(), true); + /// assert_eq!( + /// IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)).ext_is_documentation(), + /// true + /// ); + /// ``` + fn ext_is_documentation(&self) -> bool { + match self { + IpAddr::V4(ip) => ip.is_documentation(), + IpAddr::V6(ip) => ip.ext_is_documentation(), + } + } +} + +pub trait Ipv4AddrExt { + fn ext_is_global(&self) -> bool; + fn ext_is_shared(&self) -> bool; + fn ext_is_ietf_protocol_assignment(&self) -> bool; + fn ext_is_benchmarking(&self) -> bool; + fn ext_is_reserved(&self) -> bool; +} + +impl Ipv4AddrExt for Ipv4Addr { + /// Returns [`true`] if the address appears to be globally routable. + /// See [iana-ipv4-special-registry][ipv4-sr]. + /// + /// The following return false: + /// + /// - private addresses (see [`is_private()`](#method.is_private)) + /// - the loopback address (see [`is_loopback()`](#method.is_loopback)) + /// - the link-local address (see [`is_link_local()`](#method.is_link_local)) + /// - the broadcast address (see [`is_broadcast()`](#method.is_broadcast)) + /// - addresses used for documentation (see [`is_documentation()`](#method.is_documentation)) + /// - the unspecified address (see [`is_unspecified()`](#method.is_unspecified)), and the whole + /// 0.0.0.0/8 block + /// - addresses reserved for future protocols (see + /// [`is_ietf_protocol_assignment()`](#method.is_ietf_protocol_assignment), except + /// `192.0.0.9/32` and `192.0.0.10/32` which are globally routable + /// - addresses reserved for future use (see [`is_reserved()`](#method.is_reserved) + /// - addresses reserved for networking devices benchmarking (see + /// [`is_benchmarking`](#method.is_benchmarking)) + /// + /// [ipv4-sr]: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv4Addr; + /// use torment_core::ip::Ipv4AddrExt; + /// + /// // private addresses are not global + /// assert_eq!(Ipv4Addr::new(10, 254, 0, 0).ext_is_global(), false); + /// assert_eq!(Ipv4Addr::new(192, 168, 10, 65).ext_is_global(), false); + /// assert_eq!(Ipv4Addr::new(172, 16, 10, 65).ext_is_global(), false); + /// + /// // the 0.0.0.0/8 block is not global + /// assert_eq!(Ipv4Addr::new(0, 1, 2, 3).ext_is_global(), false); + /// // in particular, the unspecified address is not global + /// assert_eq!(Ipv4Addr::new(0, 0, 0, 0).ext_is_global(), false); + /// + /// // the loopback address is not global + /// assert_eq!(Ipv4Addr::new(127, 0, 0, 1).ext_is_global(), false); + /// + /// // link local addresses are not global + /// assert_eq!(Ipv4Addr::new(169, 254, 45, 1).ext_is_global(), false); + /// + /// // the broadcast address is not global + /// assert_eq!(Ipv4Addr::new(255, 255, 255, 255).ext_is_global(), false); + /// + /// // the address space designated for documentation is not global + /// assert_eq!(Ipv4Addr::new(192, 0, 2, 255).ext_is_global(), false); + /// assert_eq!(Ipv4Addr::new(198, 51, 100, 65).ext_is_global(), false); + /// assert_eq!(Ipv4Addr::new(203, 0, 113, 6).ext_is_global(), false); + /// + /// // shared addresses are not global + /// assert_eq!(Ipv4Addr::new(100, 100, 0, 0).ext_is_global(), false); + /// + /// // addresses reserved for protocol assignment are not global + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 0).ext_is_global(), false); + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 255).ext_is_global(), false); + /// + /// // addresses reserved for future use are not global + /// assert_eq!(Ipv4Addr::new(250, 10, 20, 30).ext_is_global(), false); + /// + /// // addresses reserved for network devices benchmarking are not global + /// assert_eq!(Ipv4Addr::new(198, 18, 0, 0).ext_is_global(), false); + /// + /// // All the other addresses are global + /// assert_eq!(Ipv4Addr::new(1, 1, 1, 1).ext_is_global(), true); + /// assert_eq!(Ipv4Addr::new(80, 9, 12, 3).ext_is_global(), true); + /// ``` + fn ext_is_global(&self) -> bool { + // check if this address is 192.0.0.9 or 192.0.0.10. These addresses are the only two + // globally routable addresses in the 192.0.0.0/24 range. + if u32::from(*self) == 0xc0000009 || u32::from(*self) == 0xc000000a { + return true; + } + !self.is_private() + && !self.is_loopback() + && !self.is_link_local() + && !self.is_broadcast() + && !self.is_documentation() + && !self.ext_is_shared() + && !self.ext_is_ietf_protocol_assignment() + && !self.ext_is_reserved() + && !self.ext_is_benchmarking() + // Make sure the address is not in 0.0.0.0/8 + && self.octets()[0] != 0 + } + + /// Returns [`true`] if this address is part of the Shared Address Space defined in + /// [IETF RFC 6598] (`100.64.0.0/10`). + /// + /// [IETF RFC 6598]: https://tools.ietf.org/html/rfc6598 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv4Addr; + /// use torment_core::ip::Ipv4AddrExt; + /// + /// assert_eq!(Ipv4Addr::new(100, 64, 0, 0).ext_is_shared(), true); + /// assert_eq!(Ipv4Addr::new(100, 127, 255, 255).ext_is_shared(), true); + /// assert_eq!(Ipv4Addr::new(100, 128, 0, 0).ext_is_shared(), false); + /// ``` + fn ext_is_shared(&self) -> bool { + self.octets()[0] == 100 && (self.octets()[1] & 0b1100_0000 == 0b0100_0000) + } + + /// Returns [`true`] if this address is part of `192.0.0.0/24`, which is reserved to + /// IANA for IETF protocol assignments, as documented in [IETF RFC 6890]. + /// + /// Note that parts of this block are in use: + /// + /// - `192.0.0.8/32` is the "IPv4 dummy address" (see [IETF RFC 7600]) + /// - `192.0.0.9/32` is the "Port Control Protocol Anycast" (see [IETF RFC 7723]) + /// - `192.0.0.10/32` is used for NAT traversal (see [IETF RFC 8155]) + /// + /// [IETF RFC 6890]: https://tools.ietf.org/html/rfc6890 + /// [IETF RFC 7600]: https://tools.ietf.org/html/rfc7600 + /// [IETF RFC 7723]: https://tools.ietf.org/html/rfc7723 + /// [IETF RFC 8155]: https://tools.ietf.org/html/rfc8155 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv4Addr; + /// use torment_core::ip::Ipv4AddrExt; + /// + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 0).ext_is_ietf_protocol_assignment(), true); + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 8).ext_is_ietf_protocol_assignment(), true); + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 9).ext_is_ietf_protocol_assignment(), true); + /// assert_eq!(Ipv4Addr::new(192, 0, 0, 255).ext_is_ietf_protocol_assignment(), true); + /// assert_eq!(Ipv4Addr::new(192, 0, 1, 0).ext_is_ietf_protocol_assignment(), false); + /// assert_eq!(Ipv4Addr::new(191, 255, 255, 255).ext_is_ietf_protocol_assignment(), false); + /// ``` + fn ext_is_ietf_protocol_assignment(&self) -> bool { + self.octets()[0] == 192 && self.octets()[1] == 0 && self.octets()[2] == 0 + } + + /// Returns [`true`] if this address part of the `198.18.0.0/15` range, which is reserved for + /// network devices benchmarking. This range is defined in [IETF RFC 2544] as `192.18.0.0` + /// through `198.19.255.255` but [errata 423] corrects it to `198.18.0.0/15`. + /// + /// [IETF RFC 2544]: https://tools.ietf.org/html/rfc2544 + /// [errata 423]: https://www.rfc-editor.org/errata/eid423 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv4Addr; + /// use torment_core::ip::Ipv4AddrExt; + /// + /// assert_eq!(Ipv4Addr::new(198, 17, 255, 255).ext_is_benchmarking(), false); + /// assert_eq!(Ipv4Addr::new(198, 18, 0, 0).ext_is_benchmarking(), true); + /// assert_eq!(Ipv4Addr::new(198, 19, 255, 255).ext_is_benchmarking(), true); + /// assert_eq!(Ipv4Addr::new(198, 20, 0, 0).ext_is_benchmarking(), false); + /// ``` + fn ext_is_benchmarking(&self) -> bool { + self.octets()[0] == 198 && (self.octets()[1] & 0xfe) == 18 + } + + /// Returns [`true`] if this address is reserved by IANA for future use. [IETF RFC 1112] + /// defines the block of reserved addresses as `240.0.0.0/4`. This range normally includes the + /// broadcast address `255.255.255.255`, but this implementation explicitly excludes it, since + /// it is obviously not reserved for future use. + /// + /// [IETF RFC 1112]: https://tools.ietf.org/html/rfc1112 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Warning + /// + /// As IANA assigns new addresses, this method will be + /// updated. This may result in non-reserved addresses being + /// treated as reserved in code that relies on an outdated version + /// of this method. + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv4Addr; + /// use torment_core::ip::Ipv4AddrExt; + /// + /// assert_eq!(Ipv4Addr::new(240, 0, 0, 0).ext_is_reserved(), true); + /// assert_eq!(Ipv4Addr::new(255, 255, 255, 254).ext_is_reserved(), true); + /// + /// assert_eq!(Ipv4Addr::new(239, 255, 255, 255).ext_is_reserved(), false); + /// // The broadcast address is not considered as reserved for future use by this implementation + /// assert_eq!(Ipv4Addr::new(255, 255, 255, 255).ext_is_reserved(), false); + /// ``` + fn ext_is_reserved(&self) -> bool { + self.octets()[0] & 240 == 240 && !self.is_broadcast() + } +} + +pub trait Ipv6AddrExt { + fn ext_is_global(&self) -> bool; + fn ext_is_unique_local(&self) -> bool; + fn ext_is_unicast_link_local_strict(&self) -> bool; + fn ext_is_unicast_link_local(&self) -> bool; + fn ext_is_unicast_site_local(&self) -> bool; + fn ext_is_unicast_global(&self) -> bool; + fn ext_is_documentation(&self) -> bool; + fn ext_multicast_scope(&self) -> Option; +} + +impl Ipv6AddrExt for Ipv6Addr { + /// Returns [`true`] if the address appears to be globally routable. + /// + /// The following return [`false`]: + /// + /// - the loopback address + /// - link-local and unique local unicast addresses + /// - interface-, link-, realm-, admin- and site-local multicast addresses + /// + /// [`true`]: ../../std/primitive.bool.html + /// [`false`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_is_global(), true); + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0x1).ext_is_global(), false); + /// assert_eq!(Ipv6Addr::new(0, 0, 0x1c9, 0, 0, 0xafc8, 0, 0x1).ext_is_global(), true); + /// ``` + fn ext_is_global(&self) -> bool { + match Ipv6AddrExt::ext_multicast_scope(self) { + Some(Ipv6MulticastScope::Global) => true, + None => self.ext_is_unicast_global(), + _ => false, + } + } + + /// Returns [`true`] if this is a unique local address (`fc00::/7`). + /// + /// This property is defined in [IETF RFC 4193]. + /// + /// [IETF RFC 4193]: https://tools.ietf.org/html/rfc4193 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_is_unique_local(), false); + /// assert_eq!(Ipv6Addr::new(0xfc02, 0, 0, 0, 0, 0, 0, 0).ext_is_unique_local(), true); + /// ``` + fn ext_is_unique_local(&self) -> bool { + (self.segments()[0] & 0xfe00) == 0xfc00 + } + + /// Returns [`true`] if the address is a unicast link-local address (`fe80::/64`). + /// + /// A common mis-conception is to think that "unicast link-local addresses start with + /// `fe80::`", but the [IETF RFC 4291] actually defines a stricter format for these addresses: + /// + /// ```no_rust + /// | 10 | + /// | bits | 54 bits | 64 bits | + /// +----------+-------------------------+----------------------------+ + /// |1111111010| 0 | interface ID | + /// +----------+-------------------------+----------------------------+ + /// ``` + /// + /// This method validates the format defined in the RFC and won't recognize the following + /// addresses such as `fe80:0:0:1::` or `fe81::` as unicast link-local addresses for example. + /// If you need a less strict validation use [`is_unicast_link_local()`] instead. + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0); + /// assert!(ip.ext_is_unicast_link_local_strict()); + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff); + /// assert!(ip.ext_is_unicast_link_local_strict()); + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 1, 0, 0, 0, 0); + /// assert!(!ip.ext_is_unicast_link_local_strict()); + /// assert!(ip.ext_is_unicast_link_local()); + /// + /// let ip = Ipv6Addr::new(0xfe81, 0, 0, 0, 0, 0, 0, 0); + /// assert!(!ip.ext_is_unicast_link_local_strict()); + /// assert!(ip.ext_is_unicast_link_local()); + /// ``` + /// + /// # See also + /// + /// - [IETF RFC 4291 section 2.5.6] + /// - [RFC 4291 errata 4406] + /// - [`is_unicast_link_local()`] + /// + /// [IETF RFC 4291]: https://tools.ietf.org/html/rfc4291 + /// [IETF RFC 4291 section 2.5.6]: https://tools.ietf.org/html/rfc4291#section-2.5.6 + /// [`true`]: ../../std/primitive.bool.html + /// [RFC 4291 errata 4406]: https://www.rfc-editor.org/errata/eid4406 + /// [`is_unicast_link_local()`]: ../../std/net/struct.Ipv6Addr.html#method.is_unicast_link_local + /// + fn ext_is_unicast_link_local_strict(&self) -> bool { + (self.segments()[0] & 0xffff) == 0xfe80 + && (self.segments()[1] & 0xffff) == 0 + && (self.segments()[2] & 0xffff) == 0 + && (self.segments()[3] & 0xffff) == 0 + } + + /// Returns [`true`] if the address is a unicast link-local address (`fe80::/10`). + /// + /// This method returns [`true`] for addresses in the range reserved by [RFC 4291 section 2.4], + /// i.e. addresses with the following format: + /// + /// ```no_rust + /// | 10 | + /// | bits | 54 bits | 64 bits | + /// +----------+-------------------------+----------------------------+ + /// |1111111010| arbitratry value | interface ID | + /// +----------+-------------------------+----------------------------+ + /// ``` + /// + /// As a result, this method consider addresses such as `fe80:0:0:1::` or `fe81::` to be + /// unicast link-local addresses, whereas [`is_unicast_link_local_strict()`] does not. If you + /// need a strict validation fully compliant with the RFC, use + /// [`is_unicast_link_local_strict()`]. + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0); + /// assert!(ip.ext_is_unicast_link_local()); + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff); + /// assert!(ip.ext_is_unicast_link_local()); + /// + /// let ip = Ipv6Addr::new(0xfe80, 0, 0, 1, 0, 0, 0, 0); + /// assert!(ip.ext_is_unicast_link_local()); + /// assert!(!ip.ext_is_unicast_link_local_strict()); + /// + /// let ip = Ipv6Addr::new(0xfe81, 0, 0, 0, 0, 0, 0, 0); + /// assert!(ip.ext_is_unicast_link_local()); + /// assert!(!ip.ext_is_unicast_link_local_strict()); + /// ``` + /// + /// # See also + /// + /// - [IETF RFC 4291 section 2.4] + /// - [RFC 4291 errata 4406] + /// + /// [IETF RFC 4291 section 2.4]: https://tools.ietf.org/html/rfc4291#section-2.4 + /// [`true`]: ../../std/primitive.bool.html + /// [RFC 4291 errata 4406]: https://www.rfc-editor.org/errata/eid4406 + /// [`is_unicast_link_local_strict()`]: ../../std/net/struct.Ipv6Addr.html#method.is_unicast_link_local_strict + /// + fn ext_is_unicast_link_local(&self) -> bool { + (self.segments()[0] & 0xffc0) == 0xfe80 + } + + /// Returns [`true`] if this is a deprecated unicast site-local address (fec0::/10). The + /// unicast site-local address format is defined in [RFC 4291 section 2.5.7] as: + /// + /// ```no_rust + /// | 10 | + /// | bits | 54 bits | 64 bits | + /// +----------+-------------------------+----------------------------+ + /// |1111111011| subnet ID | interface ID | + /// +----------+-------------------------+----------------------------+ + /// ``` + /// + /// [`true`]: ../../std/primitive.bool.html + /// [RFC 4291 section 2.5.7]: https://tools.ietf.org/html/rfc4291#section-2.5.7 + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// assert_eq!( + /// Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_is_unicast_site_local(), + /// false + /// ); + /// assert_eq!(Ipv6Addr::new(0xfec2, 0, 0, 0, 0, 0, 0, 0).ext_is_unicast_site_local(), true); + /// ``` + /// + /// # Warning + /// + /// As per [RFC 3879], the whole `FEC0::/10` prefix is + /// deprecated. New software must not support site-local + /// addresses. + /// + /// [RFC 3879]: https://tools.ietf.org/html/rfc3879 + fn ext_is_unicast_site_local(&self) -> bool { + (self.segments()[0] & 0xffc0) == 0xfec0 + } + + /// Returns [`true`] if the address is a globally routable unicast address. + /// + /// The following return false: + /// + /// - the loopback address + /// - the link-local addresses + /// - unique local addresses + /// - the unspecified address + /// - the address range reserved for documentation + /// + /// This method returns [`true`] for site-local addresses as per [RFC 4291 section 2.5.7] + /// + /// ```no_rust + /// The special behavior of [the site-local unicast] prefix defined in [RFC3513] must no longer + /// be supported in new implementations (i.e., new implementations must treat this prefix as + /// Global Unicast). + /// ``` + /// + /// [`true`]: ../../std/primitive.bool.html + /// [RFC 4291 section 2.5.7]: https://tools.ietf.org/html/rfc4291#section-2.5.7 + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// assert_eq!(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0).ext_is_unicast_global(), false); + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_is_unicast_global(), true); + /// ``` + fn ext_is_unicast_global(&self) -> bool { + !self.is_multicast() + && !self.is_loopback() + && !self.ext_is_unicast_link_local() + && !self.ext_is_unique_local() + && !self.is_unspecified() + && !self.ext_is_documentation() + } + + /// Returns [`true`] if this is an address reserved for documentation + /// (2001:db8::/32). + /// + /// This property is defined in [IETF RFC 3849]. + /// + /// [IETF RFC 3849]: https://tools.ietf.org/html/rfc3849 + /// [`true`]: ../../std/primitive.bool.html + /// + /// # Examples + /// + /// ``` + /// use std::net::Ipv6Addr; + /// use torment_core::ip::Ipv6AddrExt; + /// + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_is_documentation(), false); + /// assert_eq!(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0).ext_is_documentation(), true); + /// ``` + fn ext_is_documentation(&self) -> bool { + (self.segments()[0] == 0x2001) && (self.segments()[1] == 0xdb8) + } + + /// Returns the address's multicast scope if the address is multicast. + /// + /// # Examples + /// + /// ``` + /// use std::net::{Ipv6Addr}; + /// use torment_core::ip::{Ipv6AddrExt, Ipv6MulticastScope}; + /// + /// assert_eq!( + /// Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 0).ext_multicast_scope(), + /// Some(Ipv6MulticastScope::Global) + /// ); + /// assert_eq!(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff).ext_multicast_scope(), None); + /// ``` + fn ext_multicast_scope(&self) -> Option { + if self.is_multicast() { + match self.segments()[0] & 0x000f { + 1 => Some(Ipv6MulticastScope::InterfaceLocal), + 2 => Some(Ipv6MulticastScope::LinkLocal), + 3 => Some(Ipv6MulticastScope::RealmLocal), + 4 => Some(Ipv6MulticastScope::AdminLocal), + 5 => Some(Ipv6MulticastScope::SiteLocal), + 8 => Some(Ipv6MulticastScope::OrganizationLocal), + 14 => Some(Ipv6MulticastScope::Global), + _ => None, + } + } else { + None + } + } +} diff --git a/torment-core/src/lib.rs b/torment-core/src/lib.rs new file mode 100644 index 0000000..8e37b20 --- /dev/null +++ b/torment-core/src/lib.rs @@ -0,0 +1,116 @@ +#![allow(dead_code)] +use crate::infohash::v1::U160; +use crate::infohash::InfoHashCapable; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::fmt::{Display, Formatter}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +pub mod infohash; +pub mod ip; +pub mod metainfo; +pub mod utils; + +pub trait CompactContact: Sized { + fn to_compact_contact(&self) -> Vec; + fn from_compact_contact>(input: T) -> Result; +} + +impl CompactContact for SocketAddr { + fn to_compact_contact(&self) -> Vec { + match self.ip() { + IpAddr::V4(ref v4) => { + let mut compact = [0u8; 6]; + compact[..4].copy_from_slice(&v4.octets()); + compact[4..].copy_from_slice(&self.port().to_be_bytes()[..]); + compact.to_vec() + } + + IpAddr::V6(ref v6) => { + let mut compact = [0u8; 18]; + compact[..16].copy_from_slice(&v6.octets()); + compact[16..].copy_from_slice(&self.port().to_be_bytes()); + compact.to_vec() + } + } + } + + fn from_compact_contact>(input: T) -> Result { + let b = input.as_ref(); + if b.len() == 6 { + let ipv4: [u8; 4] = b[..4].try_into().unwrap(); + + Ok(SocketAddr::new( + Ipv4Addr::from(ipv4).into(), + u16::from_be_bytes(b[4..].try_into().unwrap()), + )) + } else if b.len() == 18 { + let ipv6: [u8; 16] = b[..16].try_into().unwrap(); + Ok(SocketAddr::new( + Ipv6Addr::from(ipv6).into(), + u16::from_be_bytes(b[16..].try_into().unwrap()), + )) + } else { + Err(ParsingError) + } + } +} + +#[derive(Debug)] +pub struct ParsingError; + +impl std::error::Error for ParsingError {} + +impl Display for ParsingError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "there was a parsing error.") + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +pub struct ContactInfo { + pub id: U160, + pub contact: SocketAddr, +} + +impl ContactInfo { + pub fn to_bytes(&self) -> Vec { + let contact = self.contact.to_compact_contact(); + let mut bytes = self.id.to_bytes().to_vec(); + bytes.extend(contact); + bytes + } + + pub fn from_bytes>(input: T) -> Result { + let b = input.as_ref(); + if b.len() == 26 || b.len() == 38 { + Ok(ContactInfo { + id: U160::from_bytes(&b[..20]).map_err(|_| ParsingError)?, + contact: SocketAddr::from_compact_contact(&b[20..])?, + }) + } else { + Err(ParsingError) + } + } +} + +#[derive(Debug, Default)] +pub struct PeerStorage {} + +#[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] +pub enum LookupFilter { + IPv6, + IPv4, + All, +} + +impl PeerStorage { + pub fn new() -> PeerStorage { + PeerStorage {} + } + + pub fn add_peers(&mut self, _info_hash: U160, _peers: Vec) {} + pub fn get_peers(&self, _info_hash: U160, _filter: LookupFilter) -> Vec { + vec![] + } +} diff --git a/torment-core/src/metainfo.rs b/torment-core/src/metainfo.rs new file mode 100644 index 0000000..0c09f16 --- /dev/null +++ b/torment-core/src/metainfo.rs @@ -0,0 +1,236 @@ +use crate::infohash::v1::U160; +use crate::infohash::InfoHashCapable; +use bytes::Bytes; +use num_traits::cast::ToPrimitive; +use ring::digest::{digest, SHA1_FOR_LEGACY_USE_ONLY}; +use serde::export::fmt::Debug; +use serde::export::Formatter; +use std::collections::HashSet; +use std::fmt::Display; +use std::string::FromUtf8Error; +use torment_bencode::{BencodeError, BencodeValue}; +use url::Url; + +#[derive(Debug)] +pub enum MetaInfoParsingError { + DecodingFailure(BencodeError), + Utf8Error(FromUtf8Error), + WrongType, + MissingEntry, +} + +impl From for MetaInfoParsingError { + fn from(err: BencodeError) -> Self { + MetaInfoParsingError::DecodingFailure(err) + } +} + +impl From for MetaInfoParsingError { + fn from(err: FromUtf8Error) -> Self { + MetaInfoParsingError::Utf8Error(err) + } +} + +impl Display for MetaInfoParsingError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +pub struct Torrent { + name: String, + announce_list: Vec>, + info: MetaInfo, + bencoded: BencodeValue, +} + +impl Debug for Torrent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Torrent") + .field("name", &self.name) + .field("announce_list", &self.announce_list) + .field("info", &self.info) + .finish() + } +} + +impl Torrent { + pub fn from_bytes>( + bytes: T, + name: Option, + ) -> Result { + let b = Bytes::from(bytes.as_ref().to_vec()); + let bencoded = BencodeValue::decode(b.clone())?; + let dict = bencoded.dict().ok_or(MetaInfoParsingError::WrongType)?; + + let info = dict + .get(b"info") + .ok_or(MetaInfoParsingError::MissingEntry)? + .dict() + .ok_or(MetaInfoParsingError::WrongType)?; + let data = info.buffer(); + let info = MetaInfo::from_bytes(data)?; + let name = dict + .get(b"name") + .and_then(|name| name.bytes()) + .map(|str| String::from_utf8(str.to_vec())) + .transpose()? + .or(name) + .unwrap_or(info.info_hash.to_string()); + + let announce_list = if let Some(announce) = dict.get(b"announce-list") { + announce + .list() + .map(|list| -> Result>, MetaInfoParsingError> { + let mut items = vec![]; + + for i in 0..list.len() { + let mut urls = HashSet::new(); + let item = list.get(i).unwrap(); + + let url_list = item.list().unwrap(); + for url_i in 0..url_list.len() { + urls.insert( + Url::parse( + &*url_list.get(url_i).unwrap().string().transpose()?.unwrap(), + ) + .unwrap(), + ); + } + + items.push(urls); + } + + Ok(items) + }) + .transpose()? + .unwrap_or(vec![]) + } else { + vec![] + }; + + Ok(Torrent { + name, + announce_list, + info, + bencoded, + }) + } +} + +pub struct MetaInfo { + info_hash: U160, + bencoded: BencodeValue, + piece_length: usize, + pieces: Bytes, + object: MetaInfoObject, +} + +#[derive(Clone, Debug)] +pub enum MetaInfoObject { + Files(Vec), + File(usize), +} + +impl Debug for MetaInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MetaInfo") + .field("info_hash", &self.info_hash) + .field("piece_length", &self.piece_length) + .field("object", &self.object) + .finish() + } +} + +impl MetaInfo { + pub fn from_bytes>(bytes: T) -> Result { + let b = Bytes::from(bytes.as_ref().to_vec()); + let bencoded = BencodeValue::decode(b)?; + let dict = bencoded.dict().ok_or(MetaInfoParsingError::WrongType)?; + + let piece_length = dict + .get(b"piece length") + .ok_or(MetaInfoParsingError::MissingEntry)? + .int() + .ok_or(MetaInfoParsingError::WrongType)? + .to_usize() + .ok_or(MetaInfoParsingError::WrongType)?; + + let object = if let Some(files) = dict.get(b"files") { + let mut meta_files = vec![]; + let files = files.list().ok_or(MetaInfoParsingError::WrongType)?; + + for file_i in 0..files.len() { + let file = files.get(file_i).unwrap(); + meta_files.push(MetaFile::from_bencoded(file)?); + } + + MetaInfoObject::Files(meta_files) + } else { + MetaInfoObject::File( + dict.get(b"length") + .ok_or(MetaInfoParsingError::MissingEntry)? + .int() + .ok_or(MetaInfoParsingError::WrongType)? + .to_usize() + .ok_or(MetaInfoParsingError::WrongType)?, + ) + }; + + let pieces = dict + .get(b"pieces") + .ok_or(MetaInfoParsingError::MissingEntry)? + .bytes() + .ok_or(MetaInfoParsingError::WrongType)?; + + let info_hash = + U160::from_bytes(digest(&SHA1_FOR_LEGACY_USE_ONLY, dict.buffer().as_ref())).unwrap(); + + Ok(MetaInfo { + info_hash, + bencoded, + piece_length, + pieces, + object, + }) + } +} + +#[derive(Debug, Clone)] +pub struct MetaFile { + length: usize, + path: Vec, +} + +impl MetaFile { + fn from_bencoded(bencoded: &BencodeValue) -> Result { + let dict = bencoded.dict().ok_or(MetaInfoParsingError::WrongType)?; + let path = dict + .get(b"path") + .ok_or(MetaInfoParsingError::MissingEntry)? + .list() + .ok_or(MetaInfoParsingError::WrongType)?; + + let mut path_ele = vec![]; + + for path_i in 0..path.len() { + let path_item = path.get(path_i).unwrap(); + path_ele.push( + path_item + .string() + .ok_or(MetaInfoParsingError::WrongType)??, + ) + } + + Ok(MetaFile { + path: path_ele, + length: dict + .get(b"length") + .ok_or(MetaInfoParsingError::MissingEntry)? + .int() + .ok_or(MetaInfoParsingError::WrongType)? + .to_usize() + .ok_or(MetaInfoParsingError::WrongType)?, + }) + } +} diff --git a/torment-core/src/utils.rs b/torment-core/src/utils.rs new file mode 100644 index 0000000..f99be75 --- /dev/null +++ b/torment-core/src/utils.rs @@ -0,0 +1,362 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::{Debug, Formatter}; +use std::time::Instant; + +#[derive(Default)] +pub struct EphemeralMap { + entry_map: BTreeMap>, + expiry_map: BTreeMap>, +} + +impl Debug for EphemeralMap { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.entry_map.iter()).finish() + } +} + +impl EphemeralMap { + pub fn new() -> Self { + EphemeralMap { + entry_map: Default::default(), + expiry_map: Default::default(), + } + } + + pub fn clean(&mut self) { + let valid = self.expiry_map.split_off(&Instant::now()); + let expired = std::mem::replace(&mut self.expiry_map, valid); + + for (_, keys) in expired { + for key in keys { + self.entry_map.remove(&key); + } + } + } + + pub fn insert(&mut self, key: K, value: V, expires_at: Instant) -> Option> { + let node = EphemeralNode { value, expires_at }; + let removed = self.remove(&key); + + self.add_expiry(key.clone(), expires_at); + self.entry_map.insert(key, node); + removed + } + + pub fn remove(&mut self, key: &K) -> Option> { + if let Some(node) = self.entry_map.remove(key) { + self.remove_expiry(key, node.expires_at); + Some(node) + } else { + None + } + } + + pub fn remove_value(&mut self, key: &K) -> Option { + self.remove(key).map(|x| x.value) + } + + fn add_expiry(&mut self, key: K, expires_at: Instant) { + self.expiry_map + .entry(expires_at) + .or_insert_with(|| BTreeSet::new()) + .insert(key); + } + + fn remove_expiry(&mut self, key: &K, expires_at: Instant) { + let remove_node = if let Some(item) = self.expiry_map.get_mut(&expires_at) { + item.remove(&key); + item.len() == 0 + } else { + false + }; + + if remove_node { + self.expiry_map.remove(&expires_at); + } + } + + pub fn iter(&self) -> impl Iterator { + self.entry_map.iter().filter_map(|(key, entry)| { + if entry.is_expired() { + None + } else { + Some((key, &entry.value)) + } + }) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.entry_map.iter_mut().filter_map(|(key, entry)| { + if entry.is_expired() { + None + } else { + Some((key, &mut entry.value)) + } + }) + } + + pub fn len(&self) -> usize { + self.iter().count() + } + + pub fn contains_key(&self, key: &K) -> bool { + self.get(key).is_some() + } + + pub fn get(&self, key: &K) -> Option<&V> { + self.get_node(key).map(|x| &x.value) + } + + pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { + self.get_node_mut(key).map(|x| &mut x.value) + } + + pub fn get_node(&self, key: &K) -> Option<&EphemeralNode> { + let node = if let Some(node) = self.entry_map.get(&key) { + node + } else { + return None; + }; + + if node.is_expired() { + return None; + } + + Some(node) + } + + fn get_node_mut(&mut self, key: &K) -> Option<&mut EphemeralNode> { + let node = if let Some(node) = self.entry_map.get_mut(&key) { + node + } else { + return None; + }; + + if node.is_expired() { + return None; + } + + Some(node) + } + + pub fn update_expiry(&mut self, key: &K, expires_at: Instant) { + let node = if let Some(node) = self.entry_map.get_mut(key) { + node + } else { + return; + }; + + let old_expiry = node.expires_at; + node.expires_at = expires_at; + + self.remove_expiry(key, old_expiry); + self.add_expiry(key.clone(), expires_at); + } + + pub fn entry(&mut self, key: K) -> EphemeralEntry<'_, K, V> { + EphemeralEntry { map: self, key } + } +} + +pub struct EphemeralNode { + expires_at: Instant, + value: V, +} + +impl Debug for EphemeralNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EphemeralNode") + .field("expires_at", &self.expires_at) + .field("value", &self.value) + .finish() + } +} + +impl EphemeralNode { + pub fn is_expired(&self) -> bool { + self.expires_at <= Instant::now() + } + + pub fn expires_at(&self) -> Instant { + self.expires_at + } + + pub fn value(&self) -> &V { + &self.value + } + + pub fn value_mut(&mut self) -> &mut V { + &mut self.value + } + + pub fn into_value(self) -> V { + self.value + } +} + +pub struct EphemeralEntry<'map, K: Ord + Clone, V> { + map: &'map mut EphemeralMap, + key: K, +} + +impl<'map, K: Ord + Clone + Debug, V> EphemeralEntry<'map, K, V> { + pub fn or_insert_with V>(self, expires_at: Instant, f: F) -> &'map mut V { + self.or_insert(expires_at, f()) + } + + pub fn or_insert(self, expires_at: Instant, default: V) -> &'map mut V { + self.map.insert(self.key.clone(), default, expires_at); + self.map.get_mut(&self.key).unwrap() + } + + pub fn and_modify(self, f: F) -> Self { + if let Some(item) = self.map.get_mut(&self.key) { + f(item) + } + + self + } +} + +impl<'map, K: Ord + Clone + Debug, V: Default> EphemeralEntry<'map, K, V> { + pub fn or_insert_default(self, expires_at: Instant) -> &'map mut V { + self.or_insert(expires_at, Default::default()) + } +} + +#[derive(Default)] +pub struct EphemeralSet(EphemeralMap); + +impl Debug for EphemeralSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map() + .entries( + self.0 + .entry_map + .iter() + .map(|(key, node)| (key, node.expires_at)), + ) + .finish() + } +} + +impl EphemeralSet { + pub fn new() -> Self { + EphemeralSet(EphemeralMap::new()) + } + + pub fn clean(&mut self) { + self.0.clean() + } + + pub fn insert(&mut self, key: K, expires_at: Instant) -> bool { + self.0.insert(key, (), expires_at).is_none() + } + + pub fn remove(&mut self, key: &K) -> bool { + self.0.remove(key).is_some() + } + + pub fn contains(&self, key: &K) -> bool { + self.0.get_node(key).is_some() + } + + pub fn remove_first(&mut self) -> Option { + loop { + let first_key = self.0.entry_map.keys().next().cloned(); + let item = if let Some(key) = first_key { + key + } else { + break; + }; + + if self.contains(&item) { + self.remove(&item); + return Some(item); + } + } + + None + } +} + +#[derive(Default)] +pub struct EphemeralQueueMap(EphemeralMap>>); + +impl Debug for EphemeralQueueMap { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.0.entry_map.iter()).finish() + } +} + +impl EphemeralQueueMap { + pub fn clean(&mut self) { + self.0.clean(); + } + + pub fn insert(&mut self, key: K, value: V, expires_at: Instant) { + let update_expiry = if let Some(node) = self.0.get_node_mut(&key) { + let mut old = std::mem::replace(&mut node.value, vec![]); + old.push(EphemeralNode { value, expires_at }); + + while let Some(old_node) = old.pop() { + if old_node.is_expired() { + continue; + } + + node.value.push(old_node) + } + + node.expires_at < expires_at + } else { + self.0.insert( + key.clone(), + vec![EphemeralNode { value, expires_at }], + expires_at, + ); + false + }; + + if update_expiry { + self.0.update_expiry(&key, expires_at); + } + } + + pub fn remove(&mut self, key: &K) { + self.0.remove(key); + } + + pub fn pop(&mut self, key: &K) -> Option { + if let Some(node) = self.0.get_node_mut(key) { + let old_values = std::mem::replace(&mut node.value, vec![]); + let mut selected = None; + let mut expiry: Option = None; + for value in old_values { + if value.is_expired() { + continue; + } + + if selected.is_none() { + selected = Some(value); + continue; + } + + if *expiry.get_or_insert(value.expires_at) > value.expires_at { + expiry = Some(value.expires_at) + } + + node.value.push(value) + } + + if !node.value.is_empty() { + self.0.update_expiry(key, expiry.unwrap()); + } else { + self.0.remove(key); + } + + selected.map(|x| x.value) + } else { + None + } + } +} diff --git a/torment-dht-node/Cargo.toml b/torment-dht-node/Cargo.toml new file mode 100644 index 0000000..40fbb59 --- /dev/null +++ b/torment-dht-node/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "torment-dht-node" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +torment-core = { path = "../torment-core" } +torment-dht = { path = "../torment-dht" } +futures = "0.3.5" +rand = "0.7.3" +async-std = { version = "1.6.3", features = ["attributes"] } +chrono = "0.4.15" +bytes = "0.5.6" +bson = "1.1.0" +serde = "1.0.115" +serde_derive = "1.0.115" \ No newline at end of file diff --git a/torment-dht-node/src/main.rs b/torment-dht-node/src/main.rs new file mode 100644 index 0000000..575709d --- /dev/null +++ b/torment-dht-node/src/main.rs @@ -0,0 +1,81 @@ +#![recursion_limit = "512"] +use async_std::net::{IpAddr, UdpSocket}; +use futures::select; +use futures::FutureExt; +use std::net::SocketAddr; +use std::pin::Pin; +use std::str::FromStr; +use std::time::{Duration, Instant}; +use torment_dht::host_node::HostNode; +use torment_dht::krpc::{FromBencode, Message, ToBencode}; + +async fn recv_from(socket: &UdpSocket) -> std::io::Result<(Vec, SocketAddr)> { + let mut buffer = [0u8; 2048]; + socket + .recv_from(&mut buffer) + .await + .map(|(len, addr)| (buffer[..len].to_vec(), addr)) +} + +#[async_std::main] +async fn main() { + let socket = UdpSocket::bind(SocketAddr::from_str("[::]:50002").unwrap()) + .await + .unwrap(); + let mut node = HostNode::new(Default::default(), Some(50002)); + node.add_bootstrap(SocketAddr::from_str("67.215.246.10:6881").unwrap(), None); + node.add_bootstrap( + SocketAddr::from_str("[2001:41d0:c:5ac:5::1]:6881").unwrap(), + None, + ); + + let mut fut_holder = recv_from(&socket).fuse(); + let mut socket_fut = unsafe { Pin::new_unchecked(&mut fut_holder) }; + let mut fut_sleep_holder = async_std::task::sleep(Duration::from_secs(10)).fuse(); + let mut event_fut = unsafe { Pin::new_unchecked(&mut fut_sleep_holder) }; + + loop { + while let Some((message, to)) = node.next() { + println!("| {} <= {:?}", to, message); + socket + .send_to(&message.to_bencode().unwrap(), to) + .await + .unwrap(); + } + + select! { + res = socket_fut => { + if let Ok((msg, from)) = res { + let from = if let IpAddr::V6(ipv6) = from.ip() { + if let Some(ipv4) = ipv6.to_ipv4() { + SocketAddr::new(IpAddr::V4(ipv4), from.port()) + } else { + from + } + } else { + from + }; + + match Message::from_bencode(&msg) { + Ok(msg) => { + println!("| {} => {:?}", from, msg); + node.process(msg, from); + }, + Err(err) => eprintln!("{} => Failed parsing UDP message: {}", from, err), + } + } + + fut_holder = recv_from(&socket).fuse(); + socket_fut = unsafe { Pin::new_unchecked(&mut fut_holder) }; + }, + _ = event_fut => { + println!("| Housekeeping"); + let instant = Instant::now(); + node.housekeeping(); + println!("| Housekeeping took {:?} (seen nodes: {}, table[ipv4={}, ipv6={}])", Instant::now() - instant, node.num_tracking_nodes(), node.num_ipv4_table_nodes(), node.num_ipv6_table_nodes()); + fut_sleep_holder = async_std::task::sleep(Duration::from_secs(10)).fuse(); + event_fut = unsafe { Pin::new_unchecked(&mut fut_sleep_holder) }; + }, + }; + } +} diff --git a/torment-dht/Cargo.toml b/torment-dht/Cargo.toml new file mode 100644 index 0000000..46a7126 --- /dev/null +++ b/torment-dht/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "torment-dht" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +torment-core = { path = "../torment-core" } +bendy = "0.3.2" +serde = "1.0.115" +serde_derive = "1.0.115" +rand = "0.7.3" \ No newline at end of file diff --git a/torment-dht/src/host_node.rs b/torment-dht/src/host_node.rs new file mode 100644 index 0000000..d4d1f8a --- /dev/null +++ b/torment-dht/src/host_node.rs @@ -0,0 +1,797 @@ +use crate::krpc::{Message, MessageBody, RequestBody, ResponseBody, Want}; +use crate::Table; +use rand::random; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{HashSet, VecDeque}; +use std::convert::TryInto; +use std::net::{IpAddr, SocketAddr}; +use std::ops::Add; +use std::rc::Rc; +use std::sync::RwLock; +use std::time::{Duration, Instant}; +use torment_core::infohash::v1::U160; +use torment_core::ip::IpAddrExt; +use torment_core::utils::{EphemeralMap, EphemeralSet}; +use torment_core::{ContactInfo, LookupFilter, PeerStorage}; + +#[derive(Debug)] +enum OpenRequest { + FindNode { target: U160 }, + GetPeers { info_hash: U160 }, + Ping, + Announce { info_hash: U160 }, +} + +const BOOTSTRAP_INTERVAL: Duration = Duration::from_secs(60); + +#[derive(Debug)] +pub struct HostNode { + id: U160, + port: Option, + bootstrap_nodes: HashSet<(SocketAddr, Option)>, + last_bootstrap: Option, + ipv4_table: Table, + ipv6_table: Table, + nodes: EphemeralMap<(U160, IpAddr, u16), PeerNode>, + queue: VecDeque<(Message, SocketAddr)>, + peer_storage: Rc>, +} + +#[derive(Debug)] +struct PeerNode { + id: U160, + addr: SocketAddr, + transactions: EphemeralMap, + pre_announced: EphemeralSet, + last_activity: Instant, + tokens: EphemeralSet, + received_tokens: EphemeralSet>, + transaction_id: u16, +} + +#[derive(Serialize, Deserialize)] +pub struct PersistentState { + id: U160, + nodes: Vec, + nodes6: Vec, +} + +fn validate_ip>(ip: T) -> bool { + IpAddrExt::ext_is_global(&ip.into()) +} + +impl PeerNode { + fn clean(&mut self) { + self.transactions.clean(); + self.tokens.clean(); + self.received_tokens.clean(); + self.pre_announced.clean(); + } + + fn contact_info(&self) -> ContactInfo { + ContactInfo { + id: self.id, + contact: self.addr, + } + } +} + +impl PeerNode { + fn create>(id: U160, addr: T) -> PeerNode { + PeerNode { + id, + addr: addr.into(), + transactions: EphemeralMap::new(), + pre_announced: Default::default(), + last_activity: Instant::now(), + tokens: Default::default(), + received_tokens: Default::default(), + transaction_id: 0, + } + } +} + +impl HostNode { + pub fn new(peer_storage: Rc>, port: Option) -> HostNode { + let id = U160::random(); + HostNode { + id, + port, + bootstrap_nodes: Default::default(), + last_bootstrap: None, + ipv4_table: Table::new_with_id(id), + ipv6_table: Table::new_with_id(id), + nodes: EphemeralMap::new(), + queue: Default::default(), + peer_storage, + } + } + + pub fn from_persistent_state( + peer_storage: Rc>, + port: Option, + state: PersistentState, + ) -> HostNode { + let mut host_node = HostNode { + id: state.id, + port, + bootstrap_nodes: Default::default(), + last_bootstrap: None, + ipv4_table: Table::new_with_id(state.id), + ipv6_table: Table::new_with_id(state.id), + nodes: EphemeralMap::new(), + queue: Default::default(), + peer_storage, + }; + + for node in state.nodes { + host_node + .ipv4_table + .add_node(node.id, node.contact.ip(), node.contact.port()); + } + + for node in state.nodes6 { + host_node + .ipv6_table + .add_node(node.id, node.contact.ip(), node.contact.port()); + } + + host_node.update_tables(); + host_node + } + + fn get_next_t(&mut self, contact_info: &ContactInfo) -> [u8; 2] { + let peer_node = self.get_peer_node(contact_info.id, contact_info.contact); + let old_id = peer_node.transaction_id; + peer_node.transaction_id = peer_node.transaction_id.overflowing_add(1).0; + old_id.to_ne_bytes() + } + + pub fn process(&mut self, message: Message, from: SocketAddr) { + match message.body { + MessageBody::Request(ref req) => self.handle_request(req, message.transaction_id, from), + MessageBody::Response(ref resp) => { + self.handle_response(resp, message.transaction_id, from) + } + _ => {} + } + } + + pub fn add_bootstrap(&mut self, socket_addr: SocketAddr, want: Option) { + self.bootstrap_nodes.insert((socket_addr, want)); + } + + fn get_k_closest_nodes(&self, id: U160, want: Option) -> Vec { + let mut nodes = vec![]; + if want.unwrap_or(Want::N4) == Want::N4 { + nodes.extend(self.ipv4_table.find_k_closest_nodes(id, None)) + } + + if want.unwrap_or(Want::N6) == Want::N6 { + nodes.extend(self.ipv6_table.find_k_closest_nodes(id, None)) + } + + nodes + } + + fn find_node(&mut self, target: U160, want: Option) { + if want.unwrap_or(Want::N4) == Want::N4 && self.ipv4_table.find_node(target).is_some() { + // noop + return; + } + + if want.unwrap_or(Want::N6) == Want::N6 && self.ipv6_table.find_node(target).is_some() { + // noop + return; + } + + for node in self.get_k_closest_nodes(target, want) { + let t = self.get_next_t(&node); + self.find_node_at(t, node.contact, target, want); + self.get_peer_node(node.id, node.contact) + .transactions + .insert( + u16::from_ne_bytes(t), + OpenRequest::FindNode { target }, + Instant::now().add(Duration::from_secs(5 * 60)), + ); + + self.notify_table_of_outgoing_activity(&node); + } + } + + fn find_node_at( + &mut self, + t: [u8; 2], + socket_addr: SocketAddr, + target: U160, + want: Option, + ) { + let message = Message::request_find_node(self.id, t, target, want); + self.queue.push_back((message, socket_addr)) + } + + fn get_peers(&mut self, info_hash: U160, want: Option) { + let nodes = self.get_k_closest_nodes(info_hash, want); + for node in nodes { + self.get_peers_from(&node, info_hash, want) + } + } + + fn get_peers_from(&mut self, node: &ContactInfo, info_hash: U160, want: Option) { + let message = Message::request_get_peers(self.id, self.get_next_t(&node), info_hash, want); + self.get_peer_node(node.id, node.contact) + .transactions + .insert( + u16::from_ne_bytes(message.transaction_id), + OpenRequest::GetPeers { info_hash }, + Instant::now().add(Duration::from_secs(5 * 60)), + ); + + self.notify_table_of_outgoing_activity(node); + self.queue.push_back((message, node.contact)) + } + + fn notify_table_of_outgoing_activity(&mut self, node: &ContactInfo) { + if node.contact.is_ipv4() { + self.ipv4_table.handle_outgoing_activity( + node.id, + node.contact.ip(), + node.contact.port(), + ); + } else { + self.ipv6_table.handle_outgoing_activity( + node.id, + node.contact.ip(), + node.contact.port(), + ); + } + } + + fn announce_peer_to(&mut self, node: &ContactInfo, info_hash: U160, token: Vec) { + let message = Message::request_announce_peer( + self.id, + self.get_next_t(&node), + info_hash, + token, + self.port.unwrap_or(0), + self.port.is_none(), + ); + self.get_peer_node(node.id, node.contact) + .transactions + .insert( + u16::from_ne_bytes(message.transaction_id), + OpenRequest::Announce { info_hash }, + Instant::now().add(Duration::from_secs(5 * 60)), + ); + + self.notify_table_of_outgoing_activity(node); + self.queue.push_back((message, node.contact)) + } + + pub fn ping(&mut self, node: &ContactInfo) { + let msg = Message::request_ping(self.id, self.get_next_t(node)); + self.get_peer_node(node.id, node.contact) + .transactions + .insert( + u16::from_ne_bytes(msg.transaction_id), + OpenRequest::Ping, + Instant::now().add(Duration::from_secs(5 * 60)), + ); + + self.notify_table_of_outgoing_activity(node); + self.queue.push_back((msg, node.contact)) + } + + fn announce_peer(&mut self, info_hash: U160) { + let mut announce_to = vec![]; + let mut peers_from = vec![]; + for (_, details) in &mut self.nodes.iter_mut() { + if let Some(token) = details.received_tokens.remove_first() { + announce_to.push((details.contact_info(), token)) + } else { + details + .pre_announced + .insert(info_hash, Instant::now().add(Duration::from_secs(5 * 60))); + peers_from.push(details.contact_info()); + } + } + + for (node, token) in announce_to { + self.announce_peer_to(&node, info_hash, token) + } + + for node in peers_from { + self.get_peers_from(&node, info_hash, None) + } + } + + fn has_node(&self, node: &ContactInfo) -> bool { + self.ipv4_table.has_node(node) || self.ipv6_table.has_node(node) + } + + fn knows_node(&self, node: &ContactInfo) -> bool { + self.nodes + .contains_key(&(node.id, node.contact.ip(), node.contact.port())) + } + + fn handle_response(&mut self, response: &ResponseBody, t: [u8; 2], from: SocketAddr) { + let from_id = response.node_id(); + if from_id == self.id { + // In this house we don't talk to imposters + return; + } + + match response { + ResponseBody::GetPeers(get_peers) => { + let node = self.get_peer_node(from_id, from); + let info_hash = if let Some(OpenRequest::GetPeers { info_hash }) = + node.transactions.remove_value(&u16::from_ne_bytes(t)) + { + info_hash + } else { + return; + }; + + let is_pre_announce = if node.pre_announced.remove(&info_hash) { + true + } else { + node.received_tokens.insert( + get_peers.token.clone(), + Instant::now().add(Duration::from_secs(5 * 60)), + ); + + false + }; + + if is_pre_announce { + let contact_info = node.contact_info(); + self.announce_peer_to(&contact_info, info_hash, get_peers.token.clone()); + return; + } + + if get_peers.values.len() > 0 { + self.peer_storage + .write() + .unwrap() + .add_peers(info_hash, get_peers.values.clone()); + } + + for node in &get_peers.nodes.nodes { + if !validate_ip(node.contact.ip()) { + continue; + } + + if self.has_node(node) { + continue; + } + + self.ipv4_table + .add_node(node.id, node.contact.ip(), node.contact.port()); + self.get_peers_from(node, info_hash, None); + } + + for node in &get_peers.nodes.nodes6 { + if !validate_ip(node.contact.ip()) { + continue; + } + + if self.has_node(node) { + continue; + } + + self.ipv6_table + .add_node(node.id, node.contact.ip(), node.contact.port()); + self.get_peers_from(node, info_hash, None); + } + } + + ResponseBody::FindNode(find_node) => { + let node = self.get_peer_node(response.node_id(), from); + + // We don't really care if we had a transaction or not, since at bootstrap we didn't know the nodes ID yet + let request = node.transactions.remove_value(&u16::from_ne_bytes(t)); + + let target = if let Some(OpenRequest::FindNode { target }) = request { + target + } else { + self.id + }; + + let poke_if_closer = |host: &mut HostNode, node: &ContactInfo| { + if !host.knows_node(node) && (node.id ^ host.id) < (from_id ^ host.id) { + let t = host.get_next_t(node); + host.find_node_at(t, node.contact, target, None); + } + }; + + for node in &find_node.nodes { + if !validate_ip(node.contact.ip()) { + continue; + } + + if self + .ipv4_table + .add_node(node.id, node.contact.ip(), node.contact.port()) + { + poke_if_closer(self, node); + } + } + + for node in &find_node.nodes6 { + if !validate_ip(node.contact.ip()) { + continue; + } + + if self + .ipv6_table + .add_node(node.id, node.contact.ip(), node.contact.port()) + { + poke_if_closer(self, node); + } + } + } + + ResponseBody::Empty(id) => { + let node = self.get_peer_node(response.node_id(), from); + node.transactions.remove_value(&u16::from_ne_bytes(t)); + + if node.addr.is_ipv4() { + self.ipv4_table.handle_activity(*id, from.ip(), from.port()); + } else { + self.ipv6_table.handle_activity(*id, from.ip(), from.port()); + } + } + } + } + + fn get_peer_node>(&mut self, id: U160, addr: T) -> &mut PeerNode { + let addr = addr.into(); + self.nodes.update_expiry( + &(id, addr.ip(), addr.port()), + Instant::now().add(Duration::from_secs(60 * 15)), + ); + self.nodes + .entry((id, addr.ip(), addr.port())) + .or_insert_with(Instant::now().add(Duration::from_secs(60 * 15)), || { + PeerNode::create(id, addr) + }) + } + + fn create_token(&mut self, id: U160, addr: SocketAddr, _info_hash: U160) -> Vec { + let token: u64 = random(); + + self.get_peer_node(id, addr).tokens.insert( + token.clone(), + Instant::now().add(Duration::from_secs(60 * 5)), + ); + + token.to_ne_bytes().to_vec() + } + + fn handle_request(&mut self, request: &RequestBody, t: [u8; 2], from: SocketAddr) { + if request.node_id() == self.id { + // In this house we don't talk to imposters + return; + } + + let contact_info = ContactInfo { + id: request.node_id(), + contact: from, + }; + + if !self.has_node(&contact_info) { + if from.is_ipv4() { + self.ipv4_table.add_incoming_node( + contact_info.id, + contact_info.contact.ip(), + contact_info.contact.port(), + ); + } else { + self.ipv6_table.add_incoming_node( + contact_info.id, + contact_info.contact.ip(), + contact_info.contact.port(), + ); + } + } + + match request { + RequestBody::GetPeers(get_peers) => { + let table = + get_peers + .want + .unwrap_or_else(|| if from.is_ipv4() { Want::N4 } else { Want::N6 }); + let peers = self.peer_storage.read().unwrap().get_peers( + get_peers.info_hash, + if table == Want::N4 { + LookupFilter::IPv4 + } else { + LookupFilter::IPv6 + }, + ); + + let token = self.create_token(get_peers.id, from, get_peers.info_hash); + if peers.len() == 0 { + self.queue.push_back(( + Message::response_get_peers_not_found( + self.id, + t, + token, + if table == Want::N4 { + self.ipv4_table.find_k_closest_nodes( + get_peers.info_hash, + Some((get_peers.id, from.ip(), from.port())), + ) + } else { + vec![] + }, + if table == Want::N6 { + self.ipv6_table.find_k_closest_nodes( + get_peers.info_hash, + Some((get_peers.id, from.ip(), from.port())), + ) + } else { + vec![] + }, + ), + from, + )) + } else { + self.queue.push_back(( + Message::response_get_peers_found(self.id, t, token, peers), + from, + )); + } + } + + RequestBody::FindNode(find_node) => { + let table = + find_node + .want + .unwrap_or_else(|| if from.is_ipv4() { Want::N4 } else { Want::N6 }); + + let nodes = if table == Want::N4 { + if let Some(node) = self.ipv4_table.find_node(find_node.target) { + vec![node] + } else { + self.ipv4_table.find_k_closest_nodes( + find_node.target, + Some((find_node.id, from.ip(), from.port())), + ) + } + } else { + vec![] + }; + + let nodes6 = if table == Want::N6 { + if let Some(node) = self.ipv6_table.find_node(find_node.target) { + vec![node] + } else { + self.ipv6_table.find_k_closest_nodes( + find_node.target, + Some((find_node.id, from.ip(), from.port())), + ) + } + } else { + vec![] + }; + + self.queue + .push_back((Message::response_find_node(self.id, t, nodes, nodes6), from)) + } + + RequestBody::Ping(id) => { + if from.is_ipv4() { + self.ipv4_table.handle_activity(*id, from.ip(), from.port()) + } else { + self.ipv6_table.handle_activity(*id, from.ip(), from.port()) + } + + self.queue + .push_back((Message::response_empty(self.id, t), from)) + } + + RequestBody::AnnouncePeer(announce) => { + let ok = if let Some(node) = + self.nodes.get_mut(&(announce.id, from.ip(), from.port())) + { + if announce.token.len() != 8 { + false + } else { + node.tokens.remove(&u64::from_ne_bytes( + announce.token[0..8].try_into().unwrap(), + )) + } + } else { + false + }; + + if !ok { + self.queue + .push_back((Message::error(t, 203, "Bad token".to_string()), from)) + } else { + self.peer_storage.write().unwrap().add_peers( + announce.info_hash, + vec![SocketAddr::new( + from.ip(), + if announce.implied_port { + from.port() + } else { + announce.port + }, + )], + ); + self.queue + .push_back((Message::response_empty(self.id, t), from)) + } + } + } + } + + pub fn next(&mut self) -> Option<(Message, SocketAddr)> { + self.queue.pop_front() + } + + fn clean(&mut self) { + for (_, node) in self.nodes.iter_mut() { + node.clean(); + } + } + + pub fn housekeeping(&mut self) { + self.clean(); + self.update_tables(); + if self + .last_bootstrap + .map(|last| last.add(BOOTSTRAP_INTERVAL) < Instant::now()) + .unwrap_or(true) + { + for (addr, want) in self.bootstrap_nodes.clone() { + self.find_node_at(random(), addr, self.id, want); + } + + self.last_bootstrap = Some(Instant::now()); + } + } + + fn update_tables(&mut self) { + self.ipv4_table.update_nodes(); + self.ipv6_table.update_nodes(); + + for item in self.ipv6_table.get_silent_nodes() { + self.ping(&item); + } + + for item in self.ipv4_table.get_silent_nodes() { + self.ping(&item); + } + } + + pub fn num_tracking_nodes(&self) -> usize { + self.nodes.len() + } + + pub fn num_ipv4_table_nodes(&self) -> usize { + self.ipv4_table.count_nodes() + } + + pub fn num_ipv6_table_nodes(&self) -> usize { + self.ipv6_table.count_nodes() + } +} + +#[cfg(test)] +mod test { + use crate::host_node::HostNode; + use crate::krpc::Message; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::rc::Rc; + use std::sync::RwLock; + use torment_core::infohash::v1::U160; + use torment_core::{ContactInfo, PeerStorage}; + + fn get_host() -> HostNode { + let peer_storage = Rc::new(RwLock::new(PeerStorage::new())); + HostNode::new(peer_storage, None) + } + + #[test] + fn ping_pong() { + let mut host = get_host(); + let t = [0u8; 2]; + let ip = Ipv4Addr::LOCALHOST; + let id = U160::random(); + let from = SocketAddr::new(IpAddr::from(ip), 4); + + host.process(Message::request_ping(id, t), from); + assert_eq!(1, host.ipv4_table.count_nodes()); + assert_eq!( + host.next(), + Some((Message::response_empty(host.id, t), from)) + ) + } + + #[test] + fn get_peers() { + let mut host = get_host(); + let t = [0u8; 2]; + let ip = Ipv4Addr::LOCALHOST; + let from = SocketAddr::new(IpAddr::from(ip), 4); + let id = U160::random(); + + host.process(Message::request_ping(id, t), from); + assert_eq!(1, host.ipv4_table.count_nodes()); + host.next(); + + let other = U160::random(); + host.process(Message::request_ping(other, t), from); + assert_eq!(2, host.ipv4_table.count_nodes()); + host.next(); + + let info_hash = U160::random(); + host.process(Message::request_get_peers(id, t, info_hash, None), from); + + let nodes = host + .next() + .unwrap() + .0 + .expect_response() + .1 + .expect_get_peers() + .nodes + .nodes; + + assert_eq!(1, nodes.len()); + assert_eq!( + &ContactInfo { + id: other, + contact: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 4), + }, + nodes.get(0).unwrap() + ); + } + + #[test] + fn announce_peer() { + let mut host = get_host(); + let t = [0u8; 2]; + let ip = Ipv4Addr::LOCALHOST; + let from = SocketAddr::new(IpAddr::from(ip), 4); + let id = U160::random(); + + host.process(Message::request_ping(id, t), from); + assert_eq!(1, host.ipv4_table.count_nodes()); + host.next(); + + let other = U160::random(); + host.process(Message::request_ping(other, t), from); + assert_eq!(2, host.ipv4_table.count_nodes()); + host.next(); + + let info_hash = U160::random(); + host.process(Message::request_get_peers(other, t, info_hash, None), from); + + let resp = host + .next() + .unwrap() + .0 + .expect_response() + .1 + .expect_get_peers(); + + host.process( + Message::request_announce_peer(other, t, info_hash, resp.token.clone(), 4, false), + from, + ); + + host.next().unwrap().0.expect_response().1.expect_empty(); + + host.process( + Message::request_announce_peer(other, t, info_hash, resp.token, 4, false), + from, + ); + + host.next().unwrap().0.expect_error(); + } +} diff --git a/torment-dht/src/krpc.rs b/torment-dht/src/krpc.rs new file mode 100644 index 0000000..eacecff --- /dev/null +++ b/torment-dht/src/krpc.rs @@ -0,0 +1,889 @@ +use bendy::decoding::{Error as DecodingError, Object}; +use bendy::encoding::{Error as EncodingError, SingleItemEncoder}; +use std::convert::TryInto; +use std::fmt::{Display, Formatter}; +use std::option::Option::Some; +use std::str::FromStr; +use torment_core::infohash::v1::U160; +use torment_core::infohash::InfoHashCapable; +use torment_core::{CompactContact, ContactInfo, ParsingError}; + +pub use bendy::decoding::FromBencode; +pub use bendy::encoding::ToBencode; +use std::net::SocketAddr; + +const VERSION: &'static [u8] = &[b'e', b't', 0, 0]; + +fn ver() -> Option { + // None + Some(String::from_utf8_lossy(VERSION).to_string()) +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Message { + pub transaction_id: [u8; 2], + pub body: MessageBody, + pub version: Option, +} + +impl Message { + pub fn expect_response(self) -> ([u8; 2], ResponseBody, Option) { + ( + self.transaction_id, + match self.body { + MessageBody::Response(resp) => resp, + b => panic!("Expected response got {}", b.name()), + }, + self.version, + ) + } + + pub fn expect_request(self) -> ([u8; 2], RequestBody, Option) { + ( + self.transaction_id, + match self.body { + MessageBody::Request(req) => req, + b => panic!("Expected request got {}", b.name()), + }, + self.version, + ) + } + + pub fn expect_error(self) -> ([u8; 2], ErrorBody, Option) { + ( + self.transaction_id, + match self.body { + MessageBody::Error(err) => err, + b => panic!("Expected error got {}", b.name()), + }, + self.version, + ) + } + + pub fn request_ping(id: U160, t: [u8; 2]) -> Message { + Message { + transaction_id: t, + body: MessageBody::Request(RequestBody::Ping(id)), + version: ver(), + } + } + + pub fn request_get_peers(id: U160, t: [u8; 2], info_hash: U160, want: Option) -> Message { + Message { + transaction_id: t, + body: MessageBody::Request(RequestBody::GetPeers(GetPeersRequest { + id, + want, + info_hash, + })), + version: ver(), + } + } + + pub fn request_find_node(id: U160, t: [u8; 2], target: U160, want: Option) -> Message { + Message { + transaction_id: t, + body: MessageBody::Request(RequestBody::FindNode(FindNodeRequest { id, want, target })), + version: ver(), + } + } + + pub fn request_announce_peer( + id: U160, + t: [u8; 2], + info_hash: U160, + token: Vec, + port: u16, + implied_port: bool, + ) -> Message { + Message { + transaction_id: t, + body: MessageBody::Request(RequestBody::AnnouncePeer(AnnouncePeerRequest { + id, + implied_port, + info_hash, + port, + token, + })), + version: ver(), + } + } + + pub fn response_get_peers_found( + id: U160, + t: [u8; 2], + token: Vec, + peers: Vec, + ) -> Message { + Message { + transaction_id: t, + body: MessageBody::Response(ResponseBody::GetPeers(GetPeersResponse { + id, + token, + values: peers, + nodes: ContactNodes::default(), + })), + version: ver(), + } + } + + pub fn response_get_peers_not_found( + id: U160, + t: [u8; 2], + token: Vec, + nodes: Vec, + nodes6: Vec, + ) -> Message { + Message { + transaction_id: t, + body: MessageBody::Response(ResponseBody::GetPeers(GetPeersResponse { + id, + token, + values: vec![], + nodes: ContactNodes { nodes, nodes6 }, + })), + version: ver(), + } + } + + pub fn response_find_node( + id: U160, + t: [u8; 2], + nodes: Vec, + nodes6: Vec, + ) -> Message { + Message { + transaction_id: t, + body: MessageBody::Response(ResponseBody::FindNode(FindNodeResponse { + id, + nodes, + nodes6, + })), + version: ver(), + } + } + + pub fn response_empty(id: U160, t: [u8; 2]) -> Message { + Message { + transaction_id: t, + body: MessageBody::Response(ResponseBody::Empty(id)), + version: ver(), + } + } + + pub fn error(t: [u8; 2], code: u16, text: String) -> Message { + Message { + transaction_id: t, + body: MessageBody::Error(ErrorBody { code, text }), + version: ver(), + } + } +} + +impl ToBencode for Message { + const MAX_DEPTH: usize = 3; + + fn encode(&self, encoder: SingleItemEncoder) -> Result<(), EncodingError> { + encoder.emit_unsorted_dict(|d| { + d.emit_pair_with(b"t", |e| e.emit_bytes(&self.transaction_id[..]))?; + + match self.body { + MessageBody::Error(ref err) => { + d.emit_pair(b"y", "e")?; + d.emit_pair_with(b"e", |d| { + d.emit_list(|l| { + l.emit_int(err.code)?; + l.emit_str(&err.text) + }) + })?; + } + + MessageBody::Request(ref req) => { + d.emit_pair(b"y", "q")?; + + match req { + RequestBody::Ping(ref p) => { + d.emit_pair(b"q", "ping")?; + d.emit_pair_with(b"a", |d| d.emit_dict(|mut d| d.emit_pair(b"id", p)))?; + } + + RequestBody::AnnouncePeer(ref ann) => { + d.emit_pair(b"q", "announce_peer")?; + d.emit_pair_with(b"a", |d| { + d.emit_unsorted_dict(|d| { + d.emit_pair(b"id", ann.id)?; + d.emit_pair( + b"implied_port", + if ann.implied_port { 1 } else { 0 }, + )?; + d.emit_pair(b"info_hash", ann.info_hash)?; + d.emit_pair(b"port", ann.port)?; + d.emit_pair_with(b"token", |v| v.emit_bytes(&ann.token)) + }) + })?; + } + + RequestBody::FindNode(ref fin) => { + d.emit_pair(b"q", "find_node")?; + d.emit_pair_with(b"a", |d| { + d.emit_unsorted_dict(|d| { + d.emit_pair(b"id", fin.id)?; + if let Some(ref want) = fin.want { + d.emit_pair( + b"want", + match want { + Want::N4 => "n4", + Want::N6 => "n6", + }, + )?; + } + + d.emit_pair(b"target", fin.target) + }) + })?; + } + + RequestBody::GetPeers(ref get) => { + d.emit_pair(b"q", "get_peers")?; + d.emit_pair_with(b"a", |d| { + d.emit_unsorted_dict(|d| { + d.emit_pair(b"id", get.id)?; + if let Some(ref want) = get.want { + d.emit_pair( + b"want", + match want { + Want::N4 => "n4", + Want::N6 => "n6", + }, + )?; + } + + d.emit_pair(b"info_hash", get.info_hash) + }) + })?; + } + }; + } + + MessageBody::Response(ref resp) => { + d.emit_pair(b"y", "r")?; + match resp { + ResponseBody::GetPeers(ref get) => { + d.emit_pair_with(b"r", |d| { + d.emit_unsorted_dict(|d| { + d.emit_pair(b"id", get.id)?; + d.emit_pair_with(b"token", |v| v.emit_bytes(&get.token))?; + if get.values.len() > 0 { + d.emit_pair_with(b"values", |e| { + e.emit_list(|l| { + for item in &get.values { + l.emit_bytes(&item.to_compact_contact())?; + } + + Ok(()) + }) + })?; + } + + if get.nodes.nodes.len() > 0 { + d.emit_pair_with(b"nodes", |v| { + v.emit_bytes(&compact_nodes(&get.nodes.nodes)) + })?; + } + + if get.nodes.nodes6.len() > 0 { + d.emit_pair_with(b"nodes6", |v| { + v.emit_bytes(&compact_nodes(&get.nodes.nodes6)) + })?; + } + + Ok(()) + }) + })?; + } + + ResponseBody::FindNode(ref find) => { + d.emit_pair_with(b"r", |d| { + d.emit_unsorted_dict(|d| { + d.emit_pair(b"id", find.id)?; + + if find.nodes.len() > 0 { + d.emit_pair_with(b"nodes", |v| { + v.emit_bytes(&compact_nodes(&find.nodes)) + })?; + } + + if find.nodes6.len() > 0 { + d.emit_pair_with(b"nodes6", |v| { + v.emit_bytes(&compact_nodes(&find.nodes6)) + })?; + } + + Ok(()) + }) + })?; + } + + ResponseBody::Empty(ref id) => { + d.emit_pair_with(b"r", |d| { + d.emit_dict(|mut d| d.emit_pair(b"id", id)) + })?; + } + } + } + } + + if let Some(ref version) = self.version { + d.emit_pair(b"v", version)?; + } + + Ok(()) + }) + } +} + +#[derive(Debug)] +enum KRPCError { + Generic(String), +} + +impl std::error::Error for KRPCError {} + +impl Display for KRPCError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + KRPCError::Generic(ref error) => write!(f, "Generic error: {}", error), + } + } +} + +impl FromBencode for Message { + fn decode_bencode_object(object: Object) -> Result + where + Self: Sized, + { + let mut dict = object.try_into_dictionary()?; + let mut t = None; + let mut y = None; + let mut v = None; + let mut q = None; + + let mut info_hash = None; + let mut target = None; + let mut id = None; + let mut token = None; + let mut implied_port = false; + let mut want = None; + let mut port = 0; + + let mut nodes = vec![]; + let mut nodes6 = vec![]; + let mut values = vec![]; + + let mut code = 0; + let mut text = String::new(); + + while let Some((name, obj)) = dict.next_pair()? { + match name { + b"y" => y = obj.try_into_bytes()?.first(), + b"t" => t = Some(obj.try_into_bytes()?.to_vec()), + b"v" => v = Some(obj.try_into_bytes()?.to_vec()), + b"q" => q = Some(obj.try_into_bytes()?.to_vec()), + b"a" => { + let mut args = obj.try_into_dictionary()?; + while let Some((arg_name, arg_obj)) = args.next_pair()? { + match arg_name { + b"info_hash" => { + info_hash = Some(U160::from_bytes(arg_obj.try_into_bytes()?)?) + } + b"id" => id = Some(U160::from_bytes(arg_obj.try_into_bytes()?)?), + b"target" => { + target = Some(U160::from_bytes(arg_obj.try_into_bytes()?)?) + } + b"token" => token = Some(arg_obj.try_into_bytes()?.to_vec()), + b"implied_port" => { + implied_port = usize::from_str(arg_obj.try_into_integer()?)? > 0 + } + b"port" => port = u16::from_str(arg_obj.try_into_integer()?)?, + + b"want" => { + want = match arg_obj.try_into_bytes() { + Ok(b"n4") => Some(Want::N4), + Ok(b"n6") => Some(Want::N6), + _ => None, + } + } + + _ => {} + } + } + } + b"r" => { + let mut ret = obj.try_into_dictionary()?; + while let Some((ret_name, ret_obj)) = ret.next_pair()? { + match ret_name { + b"id" => id = Some(U160::from_bytes(ret_obj.try_into_bytes()?)?), + b"token" => token = Some(ret_obj.try_into_bytes()?.to_vec()), + b"values" => { + let mut list = ret_obj.try_into_list()?; + while let Some(value) = list.next_object()? { + values.push(SocketAddr::from_compact_contact( + value.try_into_bytes()?, + )?); + } + } + + b"nodes" => { + nodes = ret_obj + .try_into_bytes()? + .chunks(26) + .filter_map(|x| ContactInfo::from_bytes(x).ok()) + .collect() + } + + b"nodes6" => { + nodes6 = ret_obj + .try_into_bytes()? + .chunks(38) + .filter_map(|x| ContactInfo::from_bytes(x).ok()) + .collect() + } + + _ => {} + } + } + } + b"e" => { + let mut err = obj.try_into_list()?; + code = + u16::from_str(err.next_object()?.ok_or(ParsingError)?.try_into_integer()?)?; + text = String::from_utf8_lossy( + err.next_object()?.ok_or(ParsingError)?.try_into_bytes()?, + ) + .to_string(); + } + + _ => {} + } + } + + let t = t.ok_or(ParsingError)?; + + let t = if t.len() == 0 { + [0, 0] + } else if t.len() == 1 { + [0, t[0]] + } else { + t[..2].try_into()? + }; + + Ok(Message { + transaction_id: t, + body: match y { + Some(b'q') => { + let q = if let Some(q) = q { + q + } else { + return Err(KRPCError::Generic("DHT Query without type".to_string()).into()); + }; + + let id = id.ok_or(ParsingError)?; + MessageBody::Request(match &q[..] { + b"ping" => RequestBody::Ping(id), + b"find_node" => RequestBody::FindNode(FindNodeRequest { + id, + want, + target: target.ok_or(ParsingError)?, + }), + b"get_peers" => RequestBody::GetPeers(GetPeersRequest { + id, + want, + info_hash: info_hash.ok_or(ParsingError)?, + }), + b"announce_peer" => RequestBody::AnnouncePeer(AnnouncePeerRequest { + id, + implied_port, + info_hash: info_hash.ok_or(ParsingError)?, + port, + token: token.ok_or(ParsingError)?, + }), + _ => Err(ParsingError)?, + }) + } + Some(b'r') => { + let id = id.ok_or(ParsingError)?; + + MessageBody::Response(if let Some(token) = token { + ResponseBody::GetPeers(GetPeersResponse { + id, + token, + values, + nodes: ContactNodes { nodes, nodes6 }, + }) + } else if nodes6.len() > 0 || nodes.len() > 0 { + ResponseBody::FindNode(FindNodeResponse { id, nodes, nodes6 }) + } else { + ResponseBody::Empty(id) + }) + } + + Some(b'e') => MessageBody::Error(ErrorBody { code, text }), + Some(x) => return Err(KRPCError::Generic(format!("Type {} is unknown", x)).into()), + None => { + return Err(KRPCError::Generic("No type given for message".to_string()).into()); + } + }, + version: v.map(|x| String::from_utf8_lossy(&x).to_string()), + }) + } +} + +fn compact_nodes(input: &Vec) -> Vec { + let mut output = vec![]; + for item in input { + output.extend(item.to_bytes()) + } + + output +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MessageBody { + Request(RequestBody), + Response(ResponseBody), + Error(ErrorBody), +} + +impl MessageBody { + fn name(&self) -> &str { + match self { + MessageBody::Request(_) => "request", + MessageBody::Response(_) => "response", + MessageBody::Error(_) => "error", + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum RequestBody { + Ping(U160), + FindNode(FindNodeRequest), + GetPeers(GetPeersRequest), + AnnouncePeer(AnnouncePeerRequest), +} + +impl RequestBody { + pub fn node_id(&self) -> U160 { + match self { + RequestBody::Ping(id) => *id, + RequestBody::FindNode(find_node) => find_node.id, + RequestBody::GetPeers(get_peers) => get_peers.id, + RequestBody::AnnouncePeer(announce_peer) => announce_peer.id, + } + } + + pub fn expect_ping(self) -> U160 { + match self { + RequestBody::Ping(id) => id, + _ => panic!("Expected ping request"), + } + } + + pub fn expect_find_node(self) -> FindNodeRequest { + match self { + RequestBody::FindNode(find_node) => find_node, + _ => panic!("Expected find node request"), + } + } + + pub fn expect_get_peers(self) -> GetPeersRequest { + match self { + RequestBody::GetPeers(get_peers) => get_peers, + _ => panic!("Expected get peers request"), + } + } + + pub fn expect_announce_peer(self) -> AnnouncePeerRequest { + match self { + RequestBody::AnnouncePeer(announce_peer) => announce_peer, + _ => panic!("Expected announce peer request"), + } + } +} + +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Copy, Hash)] +pub enum Want { + N6, + N4, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FindNodeRequest { + pub id: U160, + pub want: Option, + pub target: U160, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct GetPeersRequest { + pub id: U160, + pub want: Option, + pub info_hash: U160, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct AnnouncePeerRequest { + pub id: U160, + pub implied_port: bool, + pub info_hash: U160, + pub port: u16, + pub token: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ResponseBody { + Empty(U160), + FindNode(FindNodeResponse), + GetPeers(GetPeersResponse), +} + +impl ResponseBody { + pub fn node_id(&self) -> U160 { + match self { + ResponseBody::Empty(id) => *id, + ResponseBody::FindNode(find_node) => find_node.id, + ResponseBody::GetPeers(get_peers) => get_peers.id, + } + } + + pub fn expect_empty(self) -> U160 { + match self { + ResponseBody::Empty(id) => id, + _ => panic!("Expected empty response"), + } + } + + pub fn expect_find_node(self) -> FindNodeResponse { + match self { + ResponseBody::FindNode(find_node) => find_node, + _ => panic!("Expected find node response"), + } + } + + pub fn expect_get_peers(self) -> GetPeersResponse { + match self { + ResponseBody::GetPeers(get_peers) => get_peers, + _ => panic!("Expected get peers response"), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct FindNodeResponse { + pub id: U160, + pub nodes: Vec, + pub nodes6: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct GetPeersResponse { + pub id: U160, + pub token: Vec, + pub values: Vec, + pub nodes: ContactNodes, +} + +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct ContactNodes { + pub nodes: Vec, + pub nodes6: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ErrorBody { + pub code: u16, + pub text: String, +} + +#[cfg(test)] +mod test { + use crate::krpc::Message; + use bendy::decoding::FromBencode; + use bendy::encoding::ToBencode; + use std::borrow::Borrow; + + #[test] + fn error_response() { + let be: &[u8] = b"d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:y1:ee"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn ping_request() { + let be: &[u8] = b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn ping_response() { + let be: &[u8] = b"d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn find_node_request() { + let be: &[u8] = b"d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn find_node_response() { + let be: &[u8] = + b"d1:rd2:id20:0123456789abcdefghij5:nodes26:abcdefghij0123456789axje.ue1:t2:aa1:y1:re"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn get_peers_request() { + let be: &[u8] = + b"d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn get_peers_response_a() { + let be: &[u8] = + b"d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn get_peers_response_b() { + let be: &[u8] = + b"d1:rd2:id20:abcdefghij01234567895:nodes26:abcdefghij0123456789axje.u5:token8:aoeusnthe1:t2:aa1:y1:re"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn announce_peer_request() { + let be: &[u8] = + b"d1:ad2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } + + #[test] + fn announce_peer_response() { + let be: &[u8] = b"d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re"; + let msg = Message::from_bencode(be).expect("de-bencoding"); + let bencoded = msg.to_bencode().expect("Failed bencoding"); + println!("{:?}", msg); + println!("{:?}", bencoded); + + assert_eq!( + be, + bencoded.borrow() as &[u8], + "left [{}] != right [{}]", + String::from_utf8_lossy(be), + String::from_utf8_lossy(&bencoded) + ) + } +} diff --git a/torment-dht/src/lib.rs b/torment-dht/src/lib.rs new file mode 100644 index 0000000..6a4bbda --- /dev/null +++ b/torment-dht/src/lib.rs @@ -0,0 +1,647 @@ +#![allow(dead_code)] + +use std::cmp::min; +use std::collections::{BTreeMap, VecDeque}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::time::{Duration, Instant}; +use torment_core::infohash::v1::U160; +use torment_core::ContactInfo; + +use std::mem; +use std::ops::Add; +use std::ops::Bound::Included; +use std::option::Option::Some; + +pub mod host_node; +pub mod krpc; + +const DHT_K: usize = 8; + +#[derive(Debug)] +pub struct Table { + id: U160, + buckets: BTreeMap, +} + +type NodeHandle = (U160, IpAddr, u16); + +impl Default for Table { + fn default() -> Self { + Table::new() + } +} + +impl Table { + pub fn new_with_id(id: U160) -> Table { + Table { + id, + buckets: { + let mut tree = BTreeMap::new(); + tree.insert(U160::MAX, Bucket::new(U160::MIN, U160::MAX)); + tree + }, + } + } + + pub fn new() -> Table { + Self::new_with_id(U160::random()) + } + + fn get_bucket_index(&self, id: U160) -> U160 { + *self.buckets.range(id..).next().unwrap().0 + } + + fn get_bucket(&self, id: U160) -> &Bucket { + self.buckets + .get(&self.get_bucket_index(id)) + .expect("DHT corrupt") + } + + fn get_bucket_mut(&mut self, id: U160) -> &mut Bucket { + self.buckets + .get_mut(&self.get_bucket_index(id)) + .expect("DHT corrupt") + } + + pub fn has_node(&self, contact_info: &ContactInfo) -> bool { + self.get_node( + contact_info.id, + contact_info.contact.ip(), + contact_info.contact.port(), + ) + .is_some() + } + + fn get_node(&self, id: U160, address: IpAddr, port: u16) -> Option<&Node> { + self.get_bucket(id).nodes.get(&(id, address, port)) + } + + pub fn find_node(&self, id: U160) -> Option { + self.get_bucket(id) + .nodes + .range(( + Included(&(id, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)), + Included(&( + id, + IpAddr::V6(Ipv6Addr::new( + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + )), + u16::MAX, + )), + )) + .next() + .map(|(_, node)| node.to_contact_info()) + } + + fn get_node_mut(&mut self, id: U160, address: IpAddr, port: u16) -> Option<&mut Node> { + self.get_bucket_mut(id).nodes.get_mut(&(id, address, port)) + } + + pub fn find_k_closest_nodes( + &self, + target: U160, + excluding: Option<(U160, IpAddr, u16)>, + ) -> Vec { + let mut nodes = vec![]; + let mut found_before = 0; + let mut found_after = 0; + + let mut before_range = self.buckets.range(..target); + while let Some((_, bucket)) = before_range.next_back() { + let info = bucket.get_contact_info(); + found_before += info.len(); + nodes.extend(info); + + if found_before >= DHT_K { + break; + } + } + + let mut after_range = self.buckets.range(target..); + while let Some((_, bucket)) = after_range.next() { + let info = bucket.get_contact_info(); + found_after += info.len(); + nodes.extend(info); + + if found_after >= DHT_K { + break; + } + } + + nodes.sort_by_key(|x| x.id ^ target); + let nodes = if let Some(exclude) = excluding { + nodes + .iter() + .filter(|x| { + x.id != exclude.0 + || x.contact.ip() != exclude.1 + || x.contact.port() != exclude.2 + }) + .copied() + .collect() + } else { + nodes + }; + + nodes[..min(8, nodes.len())].to_vec() + } + + pub fn handle_activity(&mut self, id: U160, address: IpAddr, port: u16) { + let bucket_has_activity = if let Some(node) = self.get_node_mut(id, address, port) { + node.handle_activity(); + + true + } else { + self.add_node(id, address, port) + }; + + if bucket_has_activity { + self.get_bucket_mut(id).last_activity = Some(Instant::now()); + } + } + + pub fn handle_outgoing_activity(&mut self, id: U160, address: IpAddr, port: u16) { + if let Some(node) = self.get_node_mut(id, address, port) { + node.handle_outgoing_activity(); + } + } + + pub fn add_incoming_node(&mut self, id: U160, address: IpAddr, port: u16) { + self.add_node(id, address, port); + self.handle_activity(id, address, port); + } + + pub fn add_node(&mut self, id: U160, address: IpAddr, port: u16) -> bool { + let own_id = self.id; + if own_id == id { + return false; + } + + let bucket = self.get_bucket_mut(id); + if bucket.add_node(id, address, port) { + return true; + } + + if !(bucket.start <= own_id + && own_id <= bucket.end + && (bucket.end - bucket.start) > U160::ONE) + { + return false; + } + + let low_end = bucket.start + (bucket.end - bucket.start).half(); + + fn split_node_map( + nodes: &mut BTreeMap, + split_at: U160, + ) -> (BTreeMap, BTreeMap) { + let mut high = BTreeMap::new(); + let mut low = BTreeMap::new(); + let keys = nodes.keys().copied().collect::>(); + for key in keys { + let node = nodes.remove(&key).unwrap(); + if node.id <= split_at { + low.insert(key, node); + } else { + high.insert(key, node); + } + } + + (low, high) + } + + fn recount_nodes_per_id(nodes: &BTreeMap) -> BTreeMap { + nodes + .values() + .map(|x| x.id) + .fold(BTreeMap::new(), |mut c, i| { + c.entry(i).and_modify(|x| *x += 1).or_insert(1); + c + }) + } + + fn get_last_activity(nodes: &BTreeMap) -> Option { + nodes.values().filter_map(|x| x.last_activity).max() + } + + let (low, high) = split_node_map(&mut bucket.nodes, low_end); + let (low_q, high_q) = split_node_map(&mut bucket.queue, low_end); + + bucket.last_activity = get_last_activity(&high); + bucket.nodes_per_id = recount_nodes_per_id(&high); + bucket.nodes = high; + bucket.queue = high_q; + let low_start = bucket.start; + bucket.start = low_end + U160::ONE; + + let mut new_bucket = Bucket::new(low_start, low_end); + new_bucket.last_activity = get_last_activity(&low); + new_bucket.nodes_per_id = recount_nodes_per_id(&low); + new_bucket.nodes = low; + new_bucket.queue = low_q; + self.buckets.insert(low_end, new_bucket); + self.add_node(id, address, port) + } + + pub fn get_silent_nodes(&self) -> Vec { + self.iter() + .filter(|node| { + if let Some(last_activity) = node.last_activity { + if last_activity.add(Duration::from_secs(60 * 15)) < Instant::now() { + return true; + } + } + + if let Some(last_outgoing_activity) = node.last_activity { + if node.last_activity.is_none() + && last_outgoing_activity.add(Duration::from_secs(60)) < Instant::now() + { + return true; + } + } + + false + }) + .map(|node| node.to_contact_info()) + .collect() + } + + pub fn update_nodes(&mut self) { + for node in self.iter_mut() { + if node + .last_activity + .map(|x| x.add(Duration::from_secs(15 * 60)) < Instant::now()) + .unwrap_or(false) + { + if let NodeState::Questioning(n) = node.state { + if n > 3 { + #[cfg(debug_assertions)] + { + if node.state != NodeState::Bad { + println!( + "Node[id={},addr={}] went from {:?} to {:?}", + node.id, + node.sock_addr(), + node.state, + NodeState::Bad + ); + } + } + + node.state = NodeState::Bad; + } + + if node + .last_outgoing_activity + .map(|x| x.add(Duration::from_secs(3 * 60)) < Instant::now()) + .unwrap_or(false) + { + #[cfg(debug_assertions)] + { + if node.state != NodeState::Bad { + println!( + "Node[id={},addr={}] went from {:?} to {:?}", + node.id, + node.sock_addr(), + node.state, + NodeState::Bad + ); + } + } + + node.state = NodeState::Bad; + } + } else { + #[cfg(debug_assertions)] + { + if node.state != NodeState::Questioning(0) { + println!( + "Node[id={},addr={}] went from {:?} to {:?}", + node.id, + node.sock_addr(), + node.state, + NodeState::Questioning(0) + ); + } + } + + node.state = NodeState::Questioning(0); + } + } + } + + for (_, bucket) in &mut self.buckets { + if !bucket.queue.is_empty() { + let old_nodes = mem::replace(&mut bucket.nodes, BTreeMap::new()); + let mut keys = bucket + .queue + .iter() + .map(|(key, node)| (key.clone(), node.first_activity)) + .collect::>(); + keys.sort_by_key(|(_, first_activity)| *first_activity); + let mut keys = VecDeque::from(keys); + + for (key, old_node) in old_nodes { + if bucket.queue.is_empty() { + bucket.nodes.insert(key, old_node); + continue; + } + + if old_node.state == NodeState::Bad { + if let Some((key, _)) = keys.pop_front() { + if let Some(node) = bucket.queue.remove(&key) { + bucket.nodes.insert(key, node); + continue; + } + } + } + + bucket.nodes.insert(key, old_node); + } + } + } + } + + pub fn count_nodes(&self) -> usize { + self.buckets.values().map(|x| x.nodes.len()).sum() + } + + fn iter(&self) -> impl Iterator { + self.buckets + .iter() + .flat_map(|(_, bucket)| bucket.nodes.iter().map(|(_, node)| node)) + } + + fn iter_mut(&mut self) -> impl Iterator { + self.buckets + .iter_mut() + .flat_map(|(_, bucket)| bucket.nodes.iter_mut().map(|(_, node)| node)) + } +} + +#[derive(Debug)] +struct Bucket { + start: U160, + end: U160, + last_activity: Option, + nodes: BTreeMap<(U160, IpAddr, u16), Node>, + nodes_per_id: BTreeMap, + queue: BTreeMap<(U160, IpAddr, u16), Node>, +} + +impl Bucket { + fn new(start: U160, end: U160) -> Bucket { + Bucket { + start, + end, + last_activity: None, + nodes: BTreeMap::new(), + nodes_per_id: Default::default(), + queue: BTreeMap::new(), + } + } + + fn get_contact_info(&self) -> Vec { + self.nodes + .iter() + .map(|(_, node)| ContactInfo { + id: node.id, + contact: SocketAddr::new(node.address, node.port), + }) + .collect() + } + + fn internal_add_node(&mut self, id: U160, address: IpAddr, port: u16) { + self.nodes_per_id + .entry(id) + .and_modify(|x| *x += 1) + .or_insert(1); + self.nodes + .insert((id, address, port), Node::new(id, address, port)); + + self.last_activity = None; + } + + fn add_node(&mut self, id: U160, address: IpAddr, port: u16) -> bool { + // Discard after 4 nodes with the same id + if self.nodes_per_id.get(&id).unwrap_or(&0) >= &4 { + return true; + } + + if self.nodes.len() < DHT_K { + self.internal_add_node(id, address, port); + + true + } else { + let mut replace = None; + let mut has_questionable = false; + for (key, node) in &self.nodes { + if node.state == NodeState::Bad { + replace = Some(*key); + break; + } + + if let NodeState::Questioning(_) = node.state { + has_questionable = true; + } + } + + if let Some(key) = replace { + self.nodes.remove(&key); + self.nodes_per_id.entry(key.0).and_modify(|x| *x -= 1); + if Some(&0usize) == self.nodes_per_id.get(&key.0) { + self.nodes_per_id.remove(&key.0); + } + + self.internal_add_node(id, address, port); + true + } else { + if has_questionable { + let len = self.queue.len(); + let entry = self + .queue + .entry((id, address, port)) + .and_modify(|node| node.handle_activity()); + + if len < DHT_K { + entry.or_insert_with(|| { + let mut queued_node = Node::new(id, address, port); + queued_node.set_queued(); + queued_node + }); + } + } + + false + } + } + } +} + +#[derive(Debug)] +struct Node { + id: U160, + address: IpAddr, + port: u16, + first_activity: Instant, + last_outgoing_activity: Option, + last_activity: Option, + is_queued: bool, + state: NodeState, +} + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] +enum NodeState { + Good, + Bad, + Questioning(u16), +} + +impl Node { + fn new(id: U160, address: IpAddr, port: u16) -> Self { + Node { + id, + address, + port, + first_activity: Instant::now(), + last_outgoing_activity: None, + last_activity: None, + is_queued: false, + state: NodeState::Questioning(0), + } + } + + fn sock_addr(&self) -> SocketAddr { + SocketAddr::new(self.address, self.port) + } + + fn handle_activity(&mut self) { + self.last_activity = Some(Instant::now()); + #[cfg(debug_assertions)] + { + if self.state != NodeState::Good { + println!( + "Node[id={},addr={}] went from {:?} to {:?}", + self.id, + self.sock_addr(), + self.state, + NodeState::Good + ); + } + } + self.state = NodeState::Good; + } + + fn handle_outgoing_activity(&mut self) { + self.last_outgoing_activity = Some(Instant::now()); + + if let NodeState::Questioning(nr) = self.state { + #[cfg(debug_assertions)] + { + if self.state != NodeState::Questioning(nr + 1) { + println!( + "Node[id={},addr={}] went from {:?} to {:?}", + self.id, + self.sock_addr(), + self.state, + NodeState::Questioning(nr + 1) + ); + } + } + + self.state = NodeState::Questioning(nr + 1); + } + } + + fn set_queued(&mut self) { + self.is_queued = true; + } + + fn set_live(&mut self) { + self.is_queued = false; + } + + fn is_queued(&self) -> bool { + self.is_queued + } + + fn to_contact_info(&self) -> ContactInfo { + ContactInfo { + id: self.id, + contact: SocketAddr::new(self.address, self.port), + } + } +} + +#[cfg(test)] +mod test { + use crate::Table; + use std::net::{IpAddr, Ipv4Addr}; + use torment_core::infohash::v1::U160; + + #[test] + fn test_table_fulfillment() { + let mut table = Table::new_with_id(U160::ZERO); + table.add_node(U160::ONE, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 1); + table.add_node(U160::ONE, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 2); + table.add_node(U160::ONE, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 3); + table.add_node(U160::ONE, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 4); + assert_eq!(4, table.count_nodes()); + // Shouldn't be added anymore since we already have 4 nodes with the same id + table.add_node(U160::ONE, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 5); + assert_eq!(4, table.count_nodes()); + + // Don't add self + table.add_node(U160::ZERO, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 5); + assert_eq!(4, table.count_nodes()); + + let two = U160::ONE + U160::ONE; + + table.add_node(two, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 5); + assert_eq!(5, table.count_nodes()); + + table.add_node(two, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 6); + table.add_node(two, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 7); + table.add_node(two, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 8); + // node_id: MAX + table.add_node(U160::MAX, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 9); + table.add_node(U160::MAX, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 10); + table.add_node(U160::MAX, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 11); + table.add_node(U160::MAX, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), 12); + table.add_node( + U160::MAX - U160::ONE, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), + 13, + ); + table.add_node( + U160::MAX - U160::ONE, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), + 14, + ); + table.add_node( + U160::MAX - U160::ONE, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), + 15, + ); + table.add_node( + U160::MAX - U160::ONE, + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), + 16, + ); + + // This one however should too, since we already know 8 nodes with U160::MIN as id + table.add_node( + U160::MAX - (U160::ONE + U160::ONE), + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 1)), + 17, + ); + assert_eq!(16, table.count_nodes()); + } +} diff --git a/torment-peer/Cargo.toml b/torment-peer/Cargo.toml new file mode 100644 index 0000000..6b48d61 --- /dev/null +++ b/torment-peer/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "torment-peer" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/torment-peer/src/lib.rs b/torment-peer/src/lib.rs new file mode 100644 index 0000000..613c696 --- /dev/null +++ b/torment-peer/src/lib.rs @@ -0,0 +1,9 @@ +struct Peer {} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +}