├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── contrib └── OpenRC │ ├── udpt.initd │ └── udpt.pre-install ├── docs ├── .gitignore ├── book.toml └── src │ ├── README.md │ ├── SUMMARY.md │ ├── api.md │ ├── build.md │ ├── config.md │ ├── tracking_modes.md │ └── usage.md ├── rustfmt.toml ├── src ├── config.rs ├── main.rs ├── server.rs ├── stackvec.rs ├── tracker.rs └── webserver.rs └── udpt.toml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://www.buymeacoffee.com/naim"] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | run: cargo build --verbose 17 | - name: Run tests 18 | run: cargo test --verbose 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - nightly 5 | - beta 6 | 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | fast_finish: true 11 | 12 | cache: cargo 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.13" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle" 51 | version = "1.0.6" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 54 | 55 | [[package]] 56 | name = "anstyle-parse" 57 | version = "0.2.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 60 | dependencies = [ 61 | "utf8parse", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle-query" 66 | version = "1.0.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 69 | dependencies = [ 70 | "windows-sys 0.52.0", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-wincon" 75 | version = "3.0.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 78 | dependencies = [ 79 | "anstyle", 80 | "windows-sys 0.52.0", 81 | ] 82 | 83 | [[package]] 84 | name = "async-compression" 85 | version = "0.4.6" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" 88 | dependencies = [ 89 | "bzip2", 90 | "futures-core", 91 | "memchr", 92 | "pin-project-lite", 93 | "tokio", 94 | ] 95 | 96 | [[package]] 97 | name = "autocfg" 98 | version = "1.1.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 101 | 102 | [[package]] 103 | name = "backtrace" 104 | version = "0.3.69" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 107 | dependencies = [ 108 | "addr2line", 109 | "cc", 110 | "cfg-if", 111 | "libc", 112 | "miniz_oxide", 113 | "object", 114 | "rustc-demangle", 115 | ] 116 | 117 | [[package]] 118 | name = "base64" 119 | version = "0.21.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 122 | 123 | [[package]] 124 | name = "binascii" 125 | version = "0.1.4" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" 128 | 129 | [[package]] 130 | name = "bincode" 131 | version = "1.3.3" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 134 | dependencies = [ 135 | "serde", 136 | ] 137 | 138 | [[package]] 139 | name = "block-buffer" 140 | version = "0.10.4" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 143 | dependencies = [ 144 | "generic-array", 145 | ] 146 | 147 | [[package]] 148 | name = "bumpalo" 149 | version = "3.15.3" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" 152 | 153 | [[package]] 154 | name = "bytes" 155 | version = "1.5.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 158 | 159 | [[package]] 160 | name = "bzip2" 161 | version = "0.4.4" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 164 | dependencies = [ 165 | "bzip2-sys", 166 | "libc", 167 | ] 168 | 169 | [[package]] 170 | name = "bzip2-sys" 171 | version = "0.1.11+1.0.8" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 174 | dependencies = [ 175 | "cc", 176 | "libc", 177 | "pkg-config", 178 | ] 179 | 180 | [[package]] 181 | name = "cc" 182 | version = "1.0.89" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" 185 | 186 | [[package]] 187 | name = "cfg-if" 188 | version = "1.0.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 191 | 192 | [[package]] 193 | name = "chrono" 194 | version = "0.4.35" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" 197 | dependencies = [ 198 | "android-tzdata", 199 | "iana-time-zone", 200 | "js-sys", 201 | "num-traits", 202 | "wasm-bindgen", 203 | "windows-targets 0.52.4", 204 | ] 205 | 206 | [[package]] 207 | name = "clap" 208 | version = "4.5.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" 211 | dependencies = [ 212 | "clap_builder", 213 | ] 214 | 215 | [[package]] 216 | name = "clap_builder" 217 | version = "4.5.2" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 220 | dependencies = [ 221 | "anstream", 222 | "anstyle", 223 | "clap_lex", 224 | "strsim", 225 | ] 226 | 227 | [[package]] 228 | name = "clap_lex" 229 | version = "0.7.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 232 | 233 | [[package]] 234 | name = "colorchoice" 235 | version = "1.0.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 238 | 239 | [[package]] 240 | name = "core-foundation-sys" 241 | version = "0.8.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 244 | 245 | [[package]] 246 | name = "cpufeatures" 247 | version = "0.2.12" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 250 | dependencies = [ 251 | "libc", 252 | ] 253 | 254 | [[package]] 255 | name = "crypto-common" 256 | version = "0.1.6" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 259 | dependencies = [ 260 | "generic-array", 261 | "typenum", 262 | ] 263 | 264 | [[package]] 265 | name = "digest" 266 | version = "0.10.7" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 269 | dependencies = [ 270 | "block-buffer", 271 | "crypto-common", 272 | ] 273 | 274 | [[package]] 275 | name = "equivalent" 276 | version = "1.0.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 279 | 280 | [[package]] 281 | name = "fern" 282 | version = "0.6.2" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" 285 | dependencies = [ 286 | "log", 287 | ] 288 | 289 | [[package]] 290 | name = "fnv" 291 | version = "1.0.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 294 | 295 | [[package]] 296 | name = "form_urlencoded" 297 | version = "1.2.1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 300 | dependencies = [ 301 | "percent-encoding", 302 | ] 303 | 304 | [[package]] 305 | name = "futures-channel" 306 | version = "0.3.30" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 309 | dependencies = [ 310 | "futures-core", 311 | "futures-sink", 312 | ] 313 | 314 | [[package]] 315 | name = "futures-core" 316 | version = "0.3.30" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 319 | 320 | [[package]] 321 | name = "futures-sink" 322 | version = "0.3.30" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 325 | 326 | [[package]] 327 | name = "futures-task" 328 | version = "0.3.30" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 331 | 332 | [[package]] 333 | name = "futures-util" 334 | version = "0.3.30" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 337 | dependencies = [ 338 | "futures-core", 339 | "futures-sink", 340 | "futures-task", 341 | "pin-project-lite", 342 | "pin-utils", 343 | ] 344 | 345 | [[package]] 346 | name = "generic-array" 347 | version = "0.14.7" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 350 | dependencies = [ 351 | "typenum", 352 | "version_check", 353 | ] 354 | 355 | [[package]] 356 | name = "gimli" 357 | version = "0.28.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 360 | 361 | [[package]] 362 | name = "h2" 363 | version = "0.3.24" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" 366 | dependencies = [ 367 | "bytes", 368 | "fnv", 369 | "futures-core", 370 | "futures-sink", 371 | "futures-util", 372 | "http", 373 | "indexmap", 374 | "slab", 375 | "tokio", 376 | "tokio-util", 377 | "tracing", 378 | ] 379 | 380 | [[package]] 381 | name = "hashbrown" 382 | version = "0.14.3" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 385 | 386 | [[package]] 387 | name = "headers" 388 | version = "0.3.9" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" 391 | dependencies = [ 392 | "base64", 393 | "bytes", 394 | "headers-core", 395 | "http", 396 | "httpdate", 397 | "mime", 398 | "sha1", 399 | ] 400 | 401 | [[package]] 402 | name = "headers-core" 403 | version = "0.2.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 406 | dependencies = [ 407 | "http", 408 | ] 409 | 410 | [[package]] 411 | name = "hermit-abi" 412 | version = "0.3.9" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 415 | 416 | [[package]] 417 | name = "http" 418 | version = "0.2.12" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 421 | dependencies = [ 422 | "bytes", 423 | "fnv", 424 | "itoa", 425 | ] 426 | 427 | [[package]] 428 | name = "http-body" 429 | version = "0.4.6" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 432 | dependencies = [ 433 | "bytes", 434 | "http", 435 | "pin-project-lite", 436 | ] 437 | 438 | [[package]] 439 | name = "httparse" 440 | version = "1.8.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 443 | 444 | [[package]] 445 | name = "httpdate" 446 | version = "1.0.3" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 449 | 450 | [[package]] 451 | name = "hyper" 452 | version = "0.14.28" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" 455 | dependencies = [ 456 | "bytes", 457 | "futures-channel", 458 | "futures-core", 459 | "futures-util", 460 | "h2", 461 | "http", 462 | "http-body", 463 | "httparse", 464 | "httpdate", 465 | "itoa", 466 | "pin-project-lite", 467 | "socket2", 468 | "tokio", 469 | "tower-service", 470 | "tracing", 471 | "want", 472 | ] 473 | 474 | [[package]] 475 | name = "iana-time-zone" 476 | version = "0.1.60" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 479 | dependencies = [ 480 | "android_system_properties", 481 | "core-foundation-sys", 482 | "iana-time-zone-haiku", 483 | "js-sys", 484 | "wasm-bindgen", 485 | "windows-core", 486 | ] 487 | 488 | [[package]] 489 | name = "iana-time-zone-haiku" 490 | version = "0.1.2" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 493 | dependencies = [ 494 | "cc", 495 | ] 496 | 497 | [[package]] 498 | name = "indexmap" 499 | version = "2.2.5" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" 502 | dependencies = [ 503 | "equivalent", 504 | "hashbrown", 505 | ] 506 | 507 | [[package]] 508 | name = "itoa" 509 | version = "1.0.10" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 512 | 513 | [[package]] 514 | name = "js-sys" 515 | version = "0.3.69" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 518 | dependencies = [ 519 | "wasm-bindgen", 520 | ] 521 | 522 | [[package]] 523 | name = "libc" 524 | version = "0.2.153" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 527 | 528 | [[package]] 529 | name = "log" 530 | version = "0.4.21" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 533 | 534 | [[package]] 535 | name = "memchr" 536 | version = "2.7.1" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 539 | 540 | [[package]] 541 | name = "mime" 542 | version = "0.3.17" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 545 | 546 | [[package]] 547 | name = "mime_guess" 548 | version = "2.0.4" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 551 | dependencies = [ 552 | "mime", 553 | "unicase", 554 | ] 555 | 556 | [[package]] 557 | name = "miniz_oxide" 558 | version = "0.7.2" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 561 | dependencies = [ 562 | "adler", 563 | ] 564 | 565 | [[package]] 566 | name = "mio" 567 | version = "0.8.11" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 570 | dependencies = [ 571 | "libc", 572 | "wasi", 573 | "windows-sys 0.48.0", 574 | ] 575 | 576 | [[package]] 577 | name = "num-traits" 578 | version = "0.2.18" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 581 | dependencies = [ 582 | "autocfg", 583 | ] 584 | 585 | [[package]] 586 | name = "num_cpus" 587 | version = "1.16.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 590 | dependencies = [ 591 | "hermit-abi", 592 | "libc", 593 | ] 594 | 595 | [[package]] 596 | name = "object" 597 | version = "0.32.2" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 600 | dependencies = [ 601 | "memchr", 602 | ] 603 | 604 | [[package]] 605 | name = "once_cell" 606 | version = "1.19.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 609 | 610 | [[package]] 611 | name = "percent-encoding" 612 | version = "2.3.1" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 615 | 616 | [[package]] 617 | name = "pin-project" 618 | version = "1.1.5" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 621 | dependencies = [ 622 | "pin-project-internal", 623 | ] 624 | 625 | [[package]] 626 | name = "pin-project-internal" 627 | version = "1.1.5" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 630 | dependencies = [ 631 | "proc-macro2", 632 | "quote", 633 | "syn", 634 | ] 635 | 636 | [[package]] 637 | name = "pin-project-lite" 638 | version = "0.2.13" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 641 | 642 | [[package]] 643 | name = "pin-utils" 644 | version = "0.1.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 647 | 648 | [[package]] 649 | name = "pkg-config" 650 | version = "0.3.30" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 653 | 654 | [[package]] 655 | name = "proc-macro2" 656 | version = "1.0.78" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 659 | dependencies = [ 660 | "unicode-ident", 661 | ] 662 | 663 | [[package]] 664 | name = "quote" 665 | version = "1.0.35" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 668 | dependencies = [ 669 | "proc-macro2", 670 | ] 671 | 672 | [[package]] 673 | name = "rustc-demangle" 674 | version = "0.1.23" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 677 | 678 | [[package]] 679 | name = "rustls-pemfile" 680 | version = "1.0.4" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 683 | dependencies = [ 684 | "base64", 685 | ] 686 | 687 | [[package]] 688 | name = "ryu" 689 | version = "1.0.17" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 692 | 693 | [[package]] 694 | name = "scoped-tls" 695 | version = "1.0.1" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 698 | 699 | [[package]] 700 | name = "serde" 701 | version = "1.0.197" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 704 | dependencies = [ 705 | "serde_derive", 706 | ] 707 | 708 | [[package]] 709 | name = "serde_derive" 710 | version = "1.0.197" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 713 | dependencies = [ 714 | "proc-macro2", 715 | "quote", 716 | "syn", 717 | ] 718 | 719 | [[package]] 720 | name = "serde_json" 721 | version = "1.0.114" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 724 | dependencies = [ 725 | "itoa", 726 | "ryu", 727 | "serde", 728 | ] 729 | 730 | [[package]] 731 | name = "serde_spanned" 732 | version = "0.6.5" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 735 | dependencies = [ 736 | "serde", 737 | ] 738 | 739 | [[package]] 740 | name = "serde_urlencoded" 741 | version = "0.7.1" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 744 | dependencies = [ 745 | "form_urlencoded", 746 | "itoa", 747 | "ryu", 748 | "serde", 749 | ] 750 | 751 | [[package]] 752 | name = "sha1" 753 | version = "0.10.6" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 756 | dependencies = [ 757 | "cfg-if", 758 | "cpufeatures", 759 | "digest", 760 | ] 761 | 762 | [[package]] 763 | name = "signal-hook-registry" 764 | version = "1.4.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 767 | dependencies = [ 768 | "libc", 769 | ] 770 | 771 | [[package]] 772 | name = "slab" 773 | version = "0.4.9" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 776 | dependencies = [ 777 | "autocfg", 778 | ] 779 | 780 | [[package]] 781 | name = "socket2" 782 | version = "0.5.6" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" 785 | dependencies = [ 786 | "libc", 787 | "windows-sys 0.52.0", 788 | ] 789 | 790 | [[package]] 791 | name = "strsim" 792 | version = "0.11.0" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 795 | 796 | [[package]] 797 | name = "syn" 798 | version = "2.0.52" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" 801 | dependencies = [ 802 | "proc-macro2", 803 | "quote", 804 | "unicode-ident", 805 | ] 806 | 807 | [[package]] 808 | name = "tokio" 809 | version = "1.36.0" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" 812 | dependencies = [ 813 | "backtrace", 814 | "bytes", 815 | "libc", 816 | "mio", 817 | "num_cpus", 818 | "pin-project-lite", 819 | "signal-hook-registry", 820 | "socket2", 821 | "tokio-macros", 822 | "windows-sys 0.48.0", 823 | ] 824 | 825 | [[package]] 826 | name = "tokio-macros" 827 | version = "2.2.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 830 | dependencies = [ 831 | "proc-macro2", 832 | "quote", 833 | "syn", 834 | ] 835 | 836 | [[package]] 837 | name = "tokio-stream" 838 | version = "0.1.14" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 841 | dependencies = [ 842 | "futures-core", 843 | "pin-project-lite", 844 | "tokio", 845 | ] 846 | 847 | [[package]] 848 | name = "tokio-util" 849 | version = "0.7.10" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 852 | dependencies = [ 853 | "bytes", 854 | "futures-core", 855 | "futures-sink", 856 | "pin-project-lite", 857 | "tokio", 858 | "tracing", 859 | ] 860 | 861 | [[package]] 862 | name = "toml" 863 | version = "0.8.10" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 866 | dependencies = [ 867 | "serde", 868 | "serde_spanned", 869 | "toml_datetime", 870 | "toml_edit", 871 | ] 872 | 873 | [[package]] 874 | name = "toml_datetime" 875 | version = "0.6.5" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 878 | dependencies = [ 879 | "serde", 880 | ] 881 | 882 | [[package]] 883 | name = "toml_edit" 884 | version = "0.22.6" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" 887 | dependencies = [ 888 | "indexmap", 889 | "serde", 890 | "serde_spanned", 891 | "toml_datetime", 892 | "winnow", 893 | ] 894 | 895 | [[package]] 896 | name = "tower-service" 897 | version = "0.3.2" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 900 | 901 | [[package]] 902 | name = "tracing" 903 | version = "0.1.40" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 906 | dependencies = [ 907 | "log", 908 | "pin-project-lite", 909 | "tracing-core", 910 | ] 911 | 912 | [[package]] 913 | name = "tracing-core" 914 | version = "0.1.32" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 917 | dependencies = [ 918 | "once_cell", 919 | ] 920 | 921 | [[package]] 922 | name = "try-lock" 923 | version = "0.2.5" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 926 | 927 | [[package]] 928 | name = "typenum" 929 | version = "1.17.0" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 932 | 933 | [[package]] 934 | name = "udpt-rs" 935 | version = "3.1.2" 936 | dependencies = [ 937 | "async-compression", 938 | "binascii", 939 | "bincode", 940 | "chrono", 941 | "clap", 942 | "fern", 943 | "log", 944 | "serde", 945 | "serde_json", 946 | "tokio", 947 | "toml", 948 | "warp", 949 | ] 950 | 951 | [[package]] 952 | name = "unicase" 953 | version = "2.7.0" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 956 | dependencies = [ 957 | "version_check", 958 | ] 959 | 960 | [[package]] 961 | name = "unicode-ident" 962 | version = "1.0.12" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 965 | 966 | [[package]] 967 | name = "utf8parse" 968 | version = "0.2.1" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 971 | 972 | [[package]] 973 | name = "version_check" 974 | version = "0.9.4" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 977 | 978 | [[package]] 979 | name = "want" 980 | version = "0.3.1" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 983 | dependencies = [ 984 | "try-lock", 985 | ] 986 | 987 | [[package]] 988 | name = "warp" 989 | version = "0.3.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" 992 | dependencies = [ 993 | "bytes", 994 | "futures-channel", 995 | "futures-util", 996 | "headers", 997 | "http", 998 | "hyper", 999 | "log", 1000 | "mime", 1001 | "mime_guess", 1002 | "percent-encoding", 1003 | "pin-project", 1004 | "rustls-pemfile", 1005 | "scoped-tls", 1006 | "serde", 1007 | "serde_json", 1008 | "serde_urlencoded", 1009 | "tokio", 1010 | "tokio-stream", 1011 | "tokio-util", 1012 | "tower-service", 1013 | "tracing", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "wasi" 1018 | version = "0.11.0+wasi-snapshot-preview1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1021 | 1022 | [[package]] 1023 | name = "wasm-bindgen" 1024 | version = "0.2.92" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1027 | dependencies = [ 1028 | "cfg-if", 1029 | "wasm-bindgen-macro", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "wasm-bindgen-backend" 1034 | version = "0.2.92" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1037 | dependencies = [ 1038 | "bumpalo", 1039 | "log", 1040 | "once_cell", 1041 | "proc-macro2", 1042 | "quote", 1043 | "syn", 1044 | "wasm-bindgen-shared", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "wasm-bindgen-macro" 1049 | version = "0.2.92" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1052 | dependencies = [ 1053 | "quote", 1054 | "wasm-bindgen-macro-support", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "wasm-bindgen-macro-support" 1059 | version = "0.2.92" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1062 | dependencies = [ 1063 | "proc-macro2", 1064 | "quote", 1065 | "syn", 1066 | "wasm-bindgen-backend", 1067 | "wasm-bindgen-shared", 1068 | ] 1069 | 1070 | [[package]] 1071 | name = "wasm-bindgen-shared" 1072 | version = "0.2.92" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1075 | 1076 | [[package]] 1077 | name = "windows-core" 1078 | version = "0.52.0" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1081 | dependencies = [ 1082 | "windows-targets 0.52.4", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "windows-sys" 1087 | version = "0.48.0" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1090 | dependencies = [ 1091 | "windows-targets 0.48.5", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "windows-sys" 1096 | version = "0.52.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1099 | dependencies = [ 1100 | "windows-targets 0.52.4", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "windows-targets" 1105 | version = "0.48.5" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1108 | dependencies = [ 1109 | "windows_aarch64_gnullvm 0.48.5", 1110 | "windows_aarch64_msvc 0.48.5", 1111 | "windows_i686_gnu 0.48.5", 1112 | "windows_i686_msvc 0.48.5", 1113 | "windows_x86_64_gnu 0.48.5", 1114 | "windows_x86_64_gnullvm 0.48.5", 1115 | "windows_x86_64_msvc 0.48.5", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "windows-targets" 1120 | version = "0.52.4" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1123 | dependencies = [ 1124 | "windows_aarch64_gnullvm 0.52.4", 1125 | "windows_aarch64_msvc 0.52.4", 1126 | "windows_i686_gnu 0.52.4", 1127 | "windows_i686_msvc 0.52.4", 1128 | "windows_x86_64_gnu 0.52.4", 1129 | "windows_x86_64_gnullvm 0.52.4", 1130 | "windows_x86_64_msvc 0.52.4", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "windows_aarch64_gnullvm" 1135 | version = "0.48.5" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1138 | 1139 | [[package]] 1140 | name = "windows_aarch64_gnullvm" 1141 | version = "0.52.4" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1144 | 1145 | [[package]] 1146 | name = "windows_aarch64_msvc" 1147 | version = "0.48.5" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1150 | 1151 | [[package]] 1152 | name = "windows_aarch64_msvc" 1153 | version = "0.52.4" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1156 | 1157 | [[package]] 1158 | name = "windows_i686_gnu" 1159 | version = "0.48.5" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1162 | 1163 | [[package]] 1164 | name = "windows_i686_gnu" 1165 | version = "0.52.4" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1168 | 1169 | [[package]] 1170 | name = "windows_i686_msvc" 1171 | version = "0.48.5" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1174 | 1175 | [[package]] 1176 | name = "windows_i686_msvc" 1177 | version = "0.52.4" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1180 | 1181 | [[package]] 1182 | name = "windows_x86_64_gnu" 1183 | version = "0.48.5" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1186 | 1187 | [[package]] 1188 | name = "windows_x86_64_gnu" 1189 | version = "0.52.4" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1192 | 1193 | [[package]] 1194 | name = "windows_x86_64_gnullvm" 1195 | version = "0.48.5" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1198 | 1199 | [[package]] 1200 | name = "windows_x86_64_gnullvm" 1201 | version = "0.52.4" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1204 | 1205 | [[package]] 1206 | name = "windows_x86_64_msvc" 1207 | version = "0.48.5" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1210 | 1211 | [[package]] 1212 | name = "windows_x86_64_msvc" 1213 | version = "0.52.4" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1216 | 1217 | [[package]] 1218 | name = "winnow" 1219 | version = "0.6.5" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" 1222 | dependencies = [ 1223 | "memchr", 1224 | ] 1225 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "udpt-rs" 3 | version = "3.1.2" 4 | authors = ["Naim A. "] 5 | description = "High performance torrent tracker" 6 | edition = "2021" 7 | 8 | [profile.release] 9 | lto = "fat" 10 | 11 | [dependencies] 12 | serde = { version = "1.0", features = ["derive"] } 13 | bincode = "1.3" 14 | warp = { version = "^0.3.6", default-features = false } 15 | tokio = { version = "1.36", features = [ 16 | "macros", 17 | "io-util", 18 | "net", 19 | "time", 20 | "rt-multi-thread", 21 | "fs", 22 | "sync", 23 | "signal", 24 | ] } 25 | binascii = "0.1" 26 | toml = "0.8.10" 27 | clap = "4.5" 28 | log = { version = "0.4", features = ["release_max_level_info"] } 29 | fern = "0.6" 30 | serde_json = "1.0" 31 | async-compression = { version = "^0.4.6", features = ["bzip2", "tokio"] } 32 | chrono = "0.4" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | 3 | ARG BUILD_ARGS 4 | 5 | COPY . /usr/src/udpt 6 | WORKDIR /usr/src/udpt 7 | 8 | RUN cargo build --release ${BUILD_ARGS} 9 | 10 | ENTRYPOINT [ "target/release/udpt-rs" ] 11 | CMD [ "-c", "/usr/src/udpt/udpt.toml" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Naim A. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UDPT 2 | _UDPT_ is a UDP based torrent tracker which fully implements [BEP-15](http://www.bittorrent.org/beps/bep_0015.html). 3 | 4 | This project was written in Rust, it is a complete rewrite of a previous C/C++ UDPT project (which is still currently available in the `v2.1` tag of the repository). 5 | 6 | ## Features 7 | * [X] UDP torrent tracking server 8 | * [X] In memory database 9 | * [X] Choice of Dynamic/Static/Private tracker modes 10 | * [X] Ability to block a torrent from being tracked 11 | * [X] HTTP REST API for management 12 | * [X] Logging 13 | * [ ] Windows Service or Linux/Unix daemon 14 | 15 | ## Getting started 16 | The easiest way is to get built binaries from [Releases](https://github.com/naim94a/udpt/releases), 17 | but building from sources should be fairly easy as well: 18 | 19 | ```commandline 20 | git clone https://github.com/naim94a/udpt.git 21 | cd udpt 22 | cargo build --release 23 | ``` 24 | 25 | ## Contributing 26 | Please report any bugs you find to our issue tracker. Ideas and feature requests are welcome as well! 27 | 28 | Any pull request targeting existing issues would be very much appreciated. 29 | 30 | ### Why was UDPT rewritten in rust? 31 | For a few reasons, 32 | 1. Rust makes it harder to make mistakes than C/C++, It provides memory safety without runtime cost. 33 | 2. Rust allows easier cross-platform development with it's powerful standard library. 34 | 3. Integrated tests and benchmarks. 35 | 36 | 37 | UDPT was originally developed for fun in 2012 by [@naim94a](https://github.com/naim94a). 38 | -------------------------------------------------------------------------------- /contrib/OpenRC/udpt.initd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | description="UDP bittorrent Tracker" 4 | supervisor=supervise-daemon 5 | command=/usr/bin/udpt 6 | command_args="-c /etc/udpt.conf" 7 | command_user="udpt:udpt" 8 | output_log=/var/log/$RC_SVCNAME.log 9 | stopsig="SIGINT" 10 | 11 | depend() { 12 | need net 13 | } 14 | 15 | start_pre() { 16 | checkpath --directory --owner $command_user --mode 0775 \ 17 | /var/lib/$RC_SVCNAME 18 | checkpath --file --owner $command_user --mode 0644 \ 19 | /var/log/$RC_SVCNAME.log 20 | } 21 | -------------------------------------------------------------------------------- /contrib/OpenRC/udpt.pre-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | addgroup -S udpt 2>/dev/null 4 | adduser -S -D -H -h /dev/null -s /sbin/nologin -g udpt udpt 2>/dev/null 5 | exit 0 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Naim A."] 3 | title = "UDPT v3" 4 | description = "UDPT v3 User Guide" 5 | 6 | [build] 7 | create-missing = false 8 | 9 | [output.html] 10 | google-analytics = "UA-11289087-19" 11 | git-repository-url = "https://github.com/naim94a/udpt" 12 | default-theme = "coal" 13 | 14 | [output.html.search] 15 | enable = false 16 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # UDPT 2 | 3 | - Project Page: [github.com/naim94a/udpt](https://github.com/naim94a/udpt) 4 | - Documentation: [naim94a.github.io/udpt](https://naim94a.github.io/udpt) 5 | 6 | UDPT is a lightweight torrent tracker that uses the UDP protocol for tracking and fully implements [BEP-15](http://bittorrent.org/beps/bep_0015.html). 7 | This project was developed with security & simplicity in mind, so it shouldn't be difficult to get a server started. 8 | 9 | Unlike most HTTP torrent-trackers, you can save about 50% bandwidth using a UDP tracker. 10 | 11 | ## Features 12 | - UDP tracking protocol 13 | - Simple [TOML](https://en.wikipedia.org/wiki/TOML) configuration 14 | - [HTTP REST API](./api.md) 15 | - Logging 16 | - Choice to run in *static* or *dynamic* modes 17 | - Blacklist torrents using the REST API 18 | - Can be built/run on many platforms 19 | - (Re)written in [Rust](https://www.rust-lang.org/) 20 | 21 | ## Licenses 22 | UDPT available under the [MIT license](https://github.com/naim94a/udpt/blob/master/LICENSE). 23 | 24 | ## About 25 | Originally written in C++ by [@naim94a](https://github.com/naim94a) in 2012 for fun. 26 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [UDPT](README.md) 4 | 5 | * [Building](./build.md) 6 | * [Configuration](./config.md) 7 | - [Tracking Modes](tracking_modes.md) 8 | * [Usage](./usage.md) 9 | * [REST API](./api.md) 10 | -------------------------------------------------------------------------------- /docs/src/api.md: -------------------------------------------------------------------------------- 1 | # REST API 2 | The REST API can help you manage UDPT with your own scripts. 3 | 4 | __**Notice:**__ 5 | The API should only be used in trusted networks. 6 | APIs should not be exposed directly to the internet, they are intended for internal use only. 7 | 8 | ## Endpoints 9 | 10 | All Endpoints require a authorization token which must be set in the configuration before running the tracker. 11 | 12 | | Method | Route | Description | 13 | | -- | -- | -- | 14 | | `GET` | /t | list all tracked torrents. Possible query parameters are:
_offset_ - The offset in the db where to start listing torrents from.
_limit_ - Maximum amount of records to retrieve (max. 1000). | 15 | | `GET` | /t/_infohash_ | get information about a specific torrent: connected peers & stats | 16 | | `DELETE` | /t/_infohash_ | drop a torrent from the database. | 17 | | `POST` | /t/_infohash_ | add/flag/unflag torrent | 18 | 19 | The payload expected for adding a torrent can be empty, flagging or unflagging a torrent has the following payload: 20 | ```json 21 | { 22 | "is_flagged": false 23 | } 24 | ``` 25 | 26 | ## Examples 27 | Listing all tracked torrents 28 | ```bash 29 | $ curl http://127.0.0.1:1212/t/?token=MyAccessToken 30 | [{"info_hash":"1234567890123456789012345678901234567890","is_flagged":true,"completed":0,"seeders":0,"leechers":0}] 31 | ``` 32 | 33 | Getting information for a specific torrent 34 | ```bash 35 | $ curl http://127.0.0.1:1212/t/1234567890123456789012345678901234567890?token=MyAccessToken 36 | {"info_hash":"1234567890123456789012345678901234567890","is_flagged":false,"completed":0,"seeders":0,"leechers":1,"peers":[[{"id":"2d7142343235302d3458295942396f5334af686b","client":"qBittorrent"},{"ip":"192.168.1.6:52391","uploaded":0,"downloaded":0,"left":0,"event":"Started","updated":672}]]} 37 | ``` 38 | 39 | Adding a torrent (non-dynamic trackers) or Unflagging a torrent: 40 | ```bash 41 | $ curl -X POST http://127.0.0.1:1212/t/1234567890123456789012345678901234567890?token=MyAccessToken -d "{\"is_flagged\": false}" -H "Content-Type: application/json" 42 | {"status":"ok"} 43 | ``` 44 | 45 | Removing a torrent: 46 | ```bash 47 | $ curl -X DELETE http://127.0.0.1:1212/t/1234567890123456789012345678901234567890?token=MyAccessToken 48 | {"status":"ok"} 49 | ``` 50 | 51 | Flagging a torrent: 52 | ```bash 53 | $ curl -X POST http://127.0.0.1:1212/t/1234567890123456789012345678901234567890?token=MyAccessToken -d "{\"is_flagged\": true}" -H "Content-Type: application/json" 54 | {"info_hash":"1234567890123456789012345678901234567890","is_flagged":true,"completed":0,"seeders":0,"leechers":0,"peers":[]} 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/src/build.md: -------------------------------------------------------------------------------- 1 | # Building UDPT 2 | If you're reading this, you're probably interested in using UDPT - so first, thanks for your interest! 3 | 4 | UDPT used to be harder to build, especially on Windows due to it's dependencies. Thanks to Rust, it's much simpler now. 5 | 6 | ## Required tools 7 | - [Git](https://git-scm.com) - Version Control 8 | - [Rust](https://www.rust-lang.org/) - Compiler toolchain & Package Manager (cargo) 9 | 10 | ### Getting the sources 11 | ``` 12 | git clone https://github.com/naim94a/udpt.git 13 | ``` 14 | 15 | If you prefer to just download the code, you can get the [latest codebase here](https://github.com/naim94a/udpt/archive/master.zip). 16 | 17 | ### Building 18 | This step will download all required dependencies (from [crates.io](https://crates.io/)) and build them as well. 19 | 20 | Building should always be done with the latest Rust compiler. 21 | 22 | ``` 23 | cd udpt 24 | cargo build --release 25 | ``` 26 | 27 | Once cargo is done building, `udpt` will be built at `target/release/udpt`. 28 | 29 | ### Running Tests 30 | UDPT comes with unit tests, they can be run with the following command: 31 | ``` 32 | cargo test 33 | ``` 34 | 35 | If a build or test fails, please submit Issues to [UDPT's issue tracker](https://github.com/naim94a/udpt/issues). -------------------------------------------------------------------------------- /docs/src/config.md: -------------------------------------------------------------------------------- 1 | # Configuring UDPT 2 | UDPT's configuration is a simple TOML file. 3 | 4 | ## Configuration 5 | At the root level, the following options are configurable: 6 | `mode` - Specifies which mode the tracker will operate in. Values can be `static`, `dynamic` or `private`. 7 | 8 | ### Root Level 9 | - `mode` - Required. Possbile Values: `private`, `static` or `dynamic`. 10 | - `log_level` - Default: `info`. Possible Values: `off`, `error`, `warning`, `info`, `debug`, `trace`. 11 | - `db_path` - Database path. If not set, database will be volatile. 12 | - `cleanup_interval` - Default: 600. Interval to run cleanup in seconds. Cleanup also saves the Database. 13 | 14 | ### `[udp]` section 15 | This section must exist. 16 | 17 | - `bind_address` - Required. This is where the UDP port will bind to. Example: `0.0.0.0:6969`. 18 | - `announce_interval` - Required. Sets the `announce_interval` that will be sent to peers (in seconds). 19 | 20 | ### `[http]` section 21 | This section is optional. 22 | 23 | - `bind_address` - Required (if section exists). The HTTP REST API will be bound to this address. It's best not to expose this address publically. Example: `127.0.0.1:1234`. 24 | 25 | ### `[http.access_tokens]` section 26 | Section is required if `[http]` section exists. 27 | 28 | In this section you can make up keys that would be user ids, and values that would be their access token. 29 | If this section is empty, the REST API will not be very useful. 30 | 31 | ## Sample Configuration 32 | ```toml 33 | mode = "dynamic" 34 | db_path = "database.json.bz2" 35 | 36 | [udp] 37 | announce_interval = 120 # Two minutes 38 | bind_address = "0.0.0.0:1212" 39 | 40 | [http] 41 | bind_address = "127.0.0.1:1212" 42 | 43 | [http.access_tokens] 44 | someone = "MyAccessToken" 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/src/tracking_modes.md: -------------------------------------------------------------------------------- 1 | # Tracking Modes 2 | UDPT currently supports Static & Dynamic tracking modes. 3 | Private tracking is planned, but isn't yet completely implemented. 4 | 5 | ## Dynamic Mode 6 | In this mode a tracker allows any torrent, even if unknown to be tracked. 7 | Trackers that run in this mode usually don't know about the contents of the tracked torrent. 8 | In addition, trackers don't usually know anything about the peers. 9 | 10 | UDPT supports dynamic mode, and allows blacklisting torrents to avoid copyright infringement. 11 | Torrents can be blacklisted (or "flagged") using the REST API. 12 | 13 | ## Static Mode 14 | In static mode, anyone can use the tracker like in dynamic mode. 15 | Except that torrents must be registered ahead of time. 16 | 17 | UDPT supports static mode, and torrents can be added or removed using the REST API. 18 | 19 | ## Private Mode 20 | Private tracking requires all peers to be authenticated. 21 | Some implementations require torrents to be registered, and some do not. 22 | This mode can be either static or dynamic with peer authentication. 23 | 24 | UDPT doesn't currently implement private mode, although there are plans to implement private tracking in the future. 25 | -------------------------------------------------------------------------------- /docs/src/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | At the moment, `udpt` doesn't have many options. Once you've modified the configuration file, you could start `udpt`: 3 | 4 | udpt -c configuration.toml -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | brace_style = "PreferSameLine" 3 | fn_params_layout = "compressed" 4 | force_multiline_blocks = true 5 | max_width = 125 6 | overflow_delimited_expr = true 7 | use_field_init_shorthand = true 8 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | pub use crate::tracker::TrackerMode; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize)] 6 | pub struct UDPConfig { 7 | bind_address: String, 8 | announce_interval: u32, 9 | } 10 | 11 | impl UDPConfig { 12 | pub fn get_address(&self) -> &str { 13 | self.bind_address.as_str() 14 | } 15 | 16 | pub fn get_announce_interval(&self) -> u32 { 17 | self.announce_interval 18 | } 19 | } 20 | 21 | #[derive(Deserialize)] 22 | pub struct HTTPConfig { 23 | bind_address: String, 24 | access_tokens: HashMap, 25 | } 26 | 27 | impl HTTPConfig { 28 | pub fn get_address(&self) -> &str { 29 | self.bind_address.as_str() 30 | } 31 | 32 | pub fn get_access_tokens(&self) -> &HashMap { 33 | &self.access_tokens 34 | } 35 | } 36 | 37 | #[derive(Deserialize)] 38 | pub struct Configuration { 39 | mode: TrackerMode, 40 | udp: UDPConfig, 41 | http: Option, 42 | log_level: Option, 43 | db_path: Option, 44 | cleanup_interval: Option, 45 | } 46 | 47 | #[derive(Debug)] 48 | pub enum ConfigError { 49 | IOError(std::io::Error), 50 | ParseError(toml::de::Error), 51 | } 52 | 53 | impl std::fmt::Display for ConfigError { 54 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 55 | match self { 56 | ConfigError::IOError(e) => e.fmt(formatter), 57 | ConfigError::ParseError(e) => e.fmt(formatter), 58 | } 59 | } 60 | } 61 | impl std::error::Error for ConfigError {} 62 | 63 | impl Configuration { 64 | pub fn load(data: &str) -> Result { 65 | toml::from_str(data) 66 | } 67 | 68 | pub fn load_file(path: &str) -> Result { 69 | match std::fs::read_to_string(path) { 70 | Err(e) => Err(ConfigError::IOError(e)), 71 | Ok(data) => { 72 | match Self::load(&data) { 73 | Ok(cfg) => Ok(cfg), 74 | Err(e) => Err(ConfigError::ParseError(e)), 75 | } 76 | } 77 | } 78 | } 79 | 80 | pub fn get_mode(&self) -> &TrackerMode { 81 | &self.mode 82 | } 83 | 84 | pub fn get_udp_config(&self) -> &UDPConfig { 85 | &self.udp 86 | } 87 | 88 | pub fn get_log_level(&self) -> &Option { 89 | &self.log_level 90 | } 91 | 92 | pub fn get_http_config(&self) -> Option<&HTTPConfig> { 93 | self.http.as_ref() 94 | } 95 | 96 | pub fn get_db_path(&self) -> &Option { 97 | &self.db_path 98 | } 99 | 100 | pub fn get_cleanup_interval(&self) -> Option { 101 | self.cleanup_interval 102 | } 103 | } 104 | 105 | impl Default for Configuration { 106 | fn default() -> Configuration { 107 | Configuration { 108 | log_level: None, 109 | mode: TrackerMode::Dynamic, 110 | udp: UDPConfig { 111 | announce_interval: 120, 112 | bind_address: String::from("0.0.0.0:6969"), 113 | }, 114 | http: None, 115 | db_path: None, 116 | cleanup_interval: None, 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueHint; 2 | use log::{error, info, trace, warn}; 3 | 4 | mod config; 5 | mod server; 6 | mod stackvec; 7 | mod tracker; 8 | mod webserver; 9 | 10 | use config::Configuration; 11 | use std::process::exit; 12 | 13 | fn setup_logging(cfg: &Configuration) { 14 | let log_level = match cfg.get_log_level() { 15 | None => log::LevelFilter::Info, 16 | Some(level) => { 17 | match level.as_str() { 18 | "off" => log::LevelFilter::Off, 19 | "trace" => log::LevelFilter::Trace, 20 | "debug" => log::LevelFilter::Debug, 21 | "info" => log::LevelFilter::Info, 22 | "warn" => log::LevelFilter::Warn, 23 | "error" => log::LevelFilter::Error, 24 | _ => { 25 | eprintln!("udpt: unknown log level encountered '{}'", level.as_str()); 26 | exit(-1); 27 | } 28 | } 29 | } 30 | }; 31 | 32 | if let Err(err) = fern::Dispatch::new() 33 | .format(|out, message, record| { 34 | out.finish(format_args!( 35 | "{} [{}][{}] {}", 36 | chrono::Local::now().format("%+"), 37 | record.target(), 38 | record.level(), 39 | message 40 | )) 41 | }) 42 | .level(log_level) 43 | .chain(std::io::stdout()) 44 | .apply() 45 | { 46 | eprintln!("udpt: failed to initialize logging. {}", err); 47 | std::process::exit(-1); 48 | } 49 | info!("logging initialized."); 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() { 54 | let parser = clap::Command::new(env!("CARGO_PKG_NAME")) 55 | .about(env!("CARGO_PKG_DESCRIPTION")) 56 | .author(env!("CARGO_PKG_AUTHORS")) 57 | .version(env!("CARGO_PKG_VERSION")) 58 | .arg( 59 | clap::Arg::new("config") 60 | .short('c') 61 | .value_hint(ValueHint::FilePath) 62 | .help("Configuration file to load.") 63 | .required(true), 64 | ); 65 | 66 | let matches = parser.get_matches(); 67 | let cfg_path = matches.get_one::("config").unwrap(); 68 | 69 | let cfg = match Configuration::load_file(cfg_path) { 70 | Ok(v) => std::sync::Arc::new(v), 71 | Err(e) => { 72 | eprintln!("udpt: failed to open configuration: {}", e); 73 | return; 74 | } 75 | }; 76 | 77 | setup_logging(&cfg); 78 | 79 | let tracker_obj = match cfg.get_db_path() { 80 | Some(path) => { 81 | let file_path = std::path::Path::new(path); 82 | if !file_path.exists() { 83 | warn!("database file \"{}\" doesn't exist.", path); 84 | tracker::TorrentTracker::new(cfg.get_mode().clone()) 85 | } else { 86 | let mut input_file = match tokio::fs::File::open(file_path).await { 87 | Ok(v) => v, 88 | Err(err) => { 89 | error!("failed to open \"{}\". error: {}", path.as_str(), err); 90 | panic!("error opening file. check logs."); 91 | } 92 | }; 93 | match tracker::TorrentTracker::load_database(cfg.get_mode().clone(), &mut input_file).await { 94 | Ok(v) => { 95 | info!("database loaded."); 96 | v 97 | } 98 | Err(err) => { 99 | error!("failed to load database. error: {}", err); 100 | panic!("failed to load database. check logs."); 101 | } 102 | } 103 | } 104 | } 105 | None => tracker::TorrentTracker::new(cfg.get_mode().clone()), 106 | }; 107 | 108 | let tracker = std::sync::Arc::new(tracker_obj); 109 | 110 | if cfg.get_http_config().is_some() { 111 | let https_tracker = tracker.clone(); 112 | let http_cfg = cfg.clone(); 113 | 114 | info!("Starting http server"); 115 | tokio::spawn(async move { 116 | let http_cfg = http_cfg.get_http_config().unwrap(); 117 | let bind_addr = http_cfg.get_address(); 118 | let tokens = http_cfg.get_access_tokens(); 119 | 120 | let server = webserver::build_server(https_tracker, tokens.clone()); 121 | server.bind(bind_addr.parse::().unwrap()).await; 122 | }); 123 | } 124 | 125 | let udp_server = server::UDPTracker::new(cfg.clone(), tracker.clone()) 126 | .await 127 | .expect("failed to bind udp socket"); 128 | 129 | trace!("Waiting for UDP packets"); 130 | let udp_server = tokio::spawn(async move { 131 | if let Err(err) = udp_server.accept_packets().await { 132 | eprintln!("error: {}", err); 133 | } 134 | }); 135 | 136 | let weak_tracker = std::sync::Arc::downgrade(&tracker); 137 | if let Some(db_path) = cfg.get_db_path() { 138 | let db_path = db_path.clone(); 139 | let interval = cfg.get_cleanup_interval().unwrap_or(600); 140 | 141 | tokio::spawn(async move { 142 | let interval = std::time::Duration::from_secs(interval); 143 | let mut interval = tokio::time::interval(interval); 144 | interval.tick().await; // first tick is immediate... 145 | loop { 146 | interval.tick().await; 147 | if let Some(tracker) = weak_tracker.upgrade() { 148 | tracker.periodic_task(&db_path).await; 149 | } else { 150 | break; 151 | } 152 | } 153 | }); 154 | } 155 | 156 | let ctrl_c = tokio::signal::ctrl_c(); 157 | 158 | tokio::select! { 159 | _ = udp_server => { warn!("udp server exited.") }, 160 | _ = ctrl_c => { info!("CTRL-C, exiting...") }, 161 | } 162 | 163 | if let Some(path) = cfg.get_db_path() { 164 | info!("saving database..."); 165 | tracker.periodic_task(path).await; 166 | } 167 | 168 | info!("goodbye."); 169 | } 170 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error, trace}; 2 | use std::io::Write; 3 | use std::net::SocketAddr; 4 | use std::sync::Arc; 5 | use tokio::net::UdpSocket; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::config::Configuration; 10 | use crate::stackvec::StackVec; 11 | use crate::tracker; 12 | use bincode::Options; 13 | 14 | // maximum MTU is usually 1500, but our stack allows us to allocate the maximum - so why not? 15 | const MAX_PACKET_SIZE: usize = 0xffff; 16 | 17 | // protocol contants 18 | const PROTOCOL_ID: u64 = 0x0000041727101980; 19 | 20 | #[repr(u32)] 21 | #[derive(Serialize, Deserialize)] 22 | enum Actions { 23 | Connect = 0, 24 | Announce = 1, 25 | Scrape = 2, 26 | Error = 3, 27 | } 28 | 29 | #[repr(u32)] 30 | #[derive(Serialize, Deserialize, Clone, Copy)] 31 | pub enum Events { 32 | None = 0, 33 | Complete = 1, 34 | Started = 2, 35 | Stopped = 3, 36 | } 37 | 38 | fn bincode_config() -> impl bincode::Options { 39 | bincode::options() 40 | .with_big_endian() 41 | .with_fixint_encoding() 42 | .allow_trailing_bytes() 43 | } 44 | 45 | fn pack_into(w: &mut W, data: &T) -> Result<(), ()> { 46 | let config = bincode_config(); 47 | 48 | match config.serialize_into(w, data) { 49 | Ok(_) => Ok(()), 50 | Err(_) => Err(()), 51 | } 52 | } 53 | 54 | fn unpack<'a, T: Deserialize<'a>>(data: &'a [u8]) -> Option { 55 | let config = bincode_config(); 56 | 57 | match config.deserialize(data) { 58 | Ok(obj) => Some(obj), 59 | Err(_) => None, 60 | } 61 | } 62 | 63 | #[derive(Serialize, Deserialize)] 64 | struct UDPRequestHeader { 65 | connection_id: u64, 66 | action: Actions, 67 | transaction_id: u32, 68 | } 69 | 70 | #[derive(Serialize, Deserialize)] 71 | struct UDPResponseHeader { 72 | action: Actions, 73 | transaction_id: u32, 74 | } 75 | 76 | #[derive(Serialize, Deserialize)] 77 | struct UDPConnectionResponse { 78 | header: UDPResponseHeader, 79 | connection_id: u64, 80 | } 81 | 82 | #[derive(Serialize, Deserialize)] 83 | struct UDPAnnounceRequest { 84 | header: UDPRequestHeader, 85 | 86 | info_hash: [u8; 20], 87 | peer_id: [u8; 20], 88 | downloaded: u64, 89 | left: u64, 90 | uploaded: u64, 91 | event: Events, 92 | ip_address: u32, 93 | key: u32, 94 | num_want: i32, 95 | port: u16, 96 | } 97 | 98 | #[derive(Serialize, Deserialize)] 99 | struct UDPAnnounceResponse { 100 | header: UDPResponseHeader, 101 | 102 | interval: u32, 103 | leechers: u32, 104 | seeders: u32, 105 | } 106 | 107 | #[derive(Serialize)] 108 | struct UDPScrapeResponseEntry { 109 | seeders: u32, 110 | completed: u32, 111 | leechers: u32, 112 | } 113 | 114 | pub struct UDPTracker { 115 | srv: tokio::net::UdpSocket, 116 | tracker: std::sync::Arc, 117 | config: Arc, 118 | } 119 | 120 | impl UDPTracker { 121 | pub async fn new( 122 | config: Arc, tracker: std::sync::Arc, 123 | ) -> Result { 124 | let cfg = config.clone(); 125 | 126 | let srv = UdpSocket::bind(cfg.get_udp_config().get_address()).await?; 127 | 128 | Ok(UDPTracker { 129 | srv, 130 | tracker, 131 | config: cfg, 132 | }) 133 | } 134 | 135 | async fn handle_packet(&self, remote_address: &SocketAddr, payload: &[u8]) { 136 | let header: UDPRequestHeader = match unpack(payload) { 137 | Some(val) => val, 138 | None => { 139 | trace!("failed to parse packet from {}", remote_address); 140 | return; 141 | } 142 | }; 143 | 144 | match header.action { 145 | Actions::Connect => self.handle_connect(remote_address, &header, payload).await, 146 | Actions::Announce => self.handle_announce(remote_address, &header, payload).await, 147 | Actions::Scrape => self.handle_scrape(remote_address, &header, payload).await, 148 | _ => { 149 | trace!("invalid action from {}", remote_address); 150 | // someone is playing around... ignore request. 151 | } 152 | } 153 | } 154 | 155 | async fn handle_connect(&self, remote_addr: &SocketAddr, header: &UDPRequestHeader, _payload: &[u8]) { 156 | if header.connection_id != PROTOCOL_ID { 157 | trace!("Bad protocol magic from {}", remote_addr); 158 | return; 159 | } 160 | 161 | // send response... 162 | let conn_id = self.get_connection_id(remote_addr); 163 | 164 | let response = UDPConnectionResponse { 165 | header: UDPResponseHeader { 166 | transaction_id: header.transaction_id, 167 | action: Actions::Connect, 168 | }, 169 | connection_id: conn_id, 170 | }; 171 | 172 | let mut payload_buffer = vec![0u8; MAX_PACKET_SIZE]; 173 | let mut payload = StackVec::from(payload_buffer.as_mut_slice()); 174 | 175 | if pack_into(&mut payload, &response).is_ok() { 176 | let _ = self.send_packet(remote_addr, payload.as_slice()).await; 177 | } 178 | } 179 | 180 | async fn handle_announce(&self, remote_addr: &SocketAddr, header: &UDPRequestHeader, payload: &[u8]) { 181 | if header.connection_id != self.get_connection_id(remote_addr) { 182 | return; 183 | } 184 | 185 | let packet: UDPAnnounceRequest = match unpack(payload) { 186 | Some(v) => v, 187 | None => { 188 | trace!("failed to unpack announce request from {}", remote_addr); 189 | return; 190 | } 191 | }; 192 | 193 | if let Ok(_plen) = bincode::serialized_size(&packet) { 194 | let plen = _plen as usize; 195 | if payload.len() > plen { 196 | let bep41_payload = &payload[plen..]; 197 | 198 | // TODO: process BEP0041 payload. 199 | trace!("BEP0041 payload of {} bytes from {}", bep41_payload.len(), remote_addr); 200 | } 201 | } 202 | 203 | if packet.ip_address != 0 { 204 | // TODO: allow configurability of ip address 205 | // for now, ignore request. 206 | trace!("announce request for other IP ignored. (from {})", remote_addr); 207 | return; 208 | } 209 | 210 | let client_addr = SocketAddr::new(remote_addr.ip(), packet.port); 211 | let info_hash = packet.info_hash.into(); 212 | 213 | let peer_id: &tracker::PeerId = tracker::PeerId::from_array(&packet.peer_id); 214 | 215 | match self 216 | .tracker 217 | .update_torrent_and_get_stats( 218 | &info_hash, 219 | peer_id, 220 | &client_addr, 221 | packet.uploaded, 222 | packet.downloaded, 223 | packet.left, 224 | packet.event, 225 | ) 226 | .await 227 | { 228 | tracker::TorrentStats::Stats { 229 | leechers, 230 | complete: _, 231 | seeders, 232 | } => { 233 | let peers = match self.tracker.get_torrent_peers(&info_hash, &client_addr).await { 234 | Some(v) => v, 235 | None => { 236 | return; 237 | } 238 | }; 239 | 240 | let mut payload_buffer = vec![0u8; MAX_PACKET_SIZE]; 241 | let mut payload = StackVec::from(&mut payload_buffer); 242 | 243 | match pack_into(&mut payload, &UDPAnnounceResponse { 244 | header: UDPResponseHeader { 245 | action: Actions::Announce, 246 | transaction_id: packet.header.transaction_id, 247 | }, 248 | seeders, 249 | interval: self.config.get_udp_config().get_announce_interval(), 250 | leechers, 251 | }) { 252 | Ok(_) => {} 253 | Err(_) => { 254 | return; 255 | } 256 | }; 257 | 258 | for peer in peers { 259 | match peer { 260 | SocketAddr::V4(ipv4) => { 261 | let _ = payload.write(&ipv4.ip().octets()); 262 | } 263 | SocketAddr::V6(ipv6) => { 264 | let _ = payload.write(&ipv6.ip().octets()); 265 | } 266 | }; 267 | 268 | let port_hton = client_addr.port().to_be(); 269 | let _ = payload.write(&[(port_hton & 0xff) as u8, ((port_hton >> 8) & 0xff) as u8]); 270 | } 271 | 272 | let _ = self.send_packet(&client_addr, payload.as_slice()).await; 273 | } 274 | tracker::TorrentStats::TorrentFlagged => { 275 | self.send_error(&client_addr, &packet.header, "torrent flagged.").await; 276 | } 277 | tracker::TorrentStats::TorrentNotRegistered => { 278 | self.send_error(&client_addr, &packet.header, "torrent not registered.").await; 279 | } 280 | } 281 | } 282 | 283 | async fn handle_scrape(&self, remote_addr: &SocketAddr, header: &UDPRequestHeader, payload: &[u8]) { 284 | if header.connection_id != self.get_connection_id(remote_addr) { 285 | return; 286 | } 287 | 288 | const MAX_SCRAPE: usize = 74; 289 | 290 | let mut response_buffer = [0u8; 8 + MAX_SCRAPE * 12]; 291 | let mut response = StackVec::from(&mut response_buffer); 292 | 293 | if pack_into(&mut response, &UDPResponseHeader { 294 | action: Actions::Scrape, 295 | transaction_id: header.transaction_id, 296 | }) 297 | .is_err() 298 | { 299 | // not much we can do... 300 | error!("failed to encode udp scrape response header."); 301 | return; 302 | } 303 | 304 | // skip first 16 bytes for header... 305 | let info_hash_array = &payload[16..]; 306 | 307 | if info_hash_array.len() % 20 != 0 { 308 | trace!("received weird length for scrape info_hash array (!mod20)."); 309 | } 310 | 311 | { 312 | let db = self.tracker.get_database().await; 313 | 314 | for torrent_index in 0..MAX_SCRAPE { 315 | let info_hash_start = torrent_index * 20; 316 | let info_hash_end = (torrent_index + 1) * 20; 317 | 318 | if info_hash_end > info_hash_array.len() { 319 | break; 320 | } 321 | 322 | let info_hash = &info_hash_array[info_hash_start..info_hash_end]; 323 | let ih = tracker::InfoHash::from(info_hash); 324 | let result = match db.get(&ih) { 325 | Some(torrent_info) => { 326 | let (seeders, completed, leechers) = torrent_info.get_stats(); 327 | 328 | UDPScrapeResponseEntry { 329 | seeders, 330 | completed, 331 | leechers, 332 | } 333 | } 334 | None => { 335 | UDPScrapeResponseEntry { 336 | seeders: 0, 337 | completed: 0, 338 | leechers: 0, 339 | } 340 | } 341 | }; 342 | 343 | if pack_into(&mut response, &result).is_err() { 344 | debug!("failed to encode scrape entry."); 345 | return; 346 | } 347 | } 348 | } 349 | 350 | // if sending fails, not much we can do... 351 | let _ = self.send_packet(remote_addr, response.as_slice()).await; 352 | } 353 | 354 | fn get_connection_id(&self, remote_address: &SocketAddr) -> u64 { 355 | match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { 356 | Ok(duration) => (duration.as_secs() / 3600) | ((remote_address.port() as u64) << 36), 357 | Err(_) => 0x8000000000000000, 358 | } 359 | } 360 | 361 | async fn send_packet(&self, remote_addr: &SocketAddr, payload: &[u8]) -> Result { 362 | match self.srv.send_to(payload, remote_addr).await { 363 | Err(err) => { 364 | debug!("failed to send a packet: {}", err); 365 | Err(err) 366 | } 367 | Ok(sz) => Ok(sz), 368 | } 369 | } 370 | 371 | async fn send_error(&self, remote_addr: &SocketAddr, header: &UDPRequestHeader, error_msg: &str) { 372 | let mut payload_buffer = vec![0u8; MAX_PACKET_SIZE]; 373 | let mut payload = StackVec::from(&mut payload_buffer); 374 | 375 | if pack_into(&mut payload, &UDPResponseHeader { 376 | transaction_id: header.transaction_id, 377 | action: Actions::Error, 378 | }) 379 | .is_ok() 380 | { 381 | let msg_bytes = Vec::from(error_msg.as_bytes()); 382 | payload.extend(msg_bytes); 383 | 384 | let _ = self.send_packet(remote_addr, payload.as_slice()).await; 385 | } 386 | } 387 | 388 | pub async fn accept_packets(self) -> Result<(), std::io::Error> { 389 | let tracker = Arc::new(self); 390 | 391 | loop { 392 | let mut packet = vec![0u8; MAX_PACKET_SIZE]; 393 | let (size, remote_address) = tracker.srv.recv_from(packet.as_mut_slice()).await?; 394 | 395 | let tracker = tracker.clone(); 396 | tokio::spawn(async move { 397 | debug!("Received {} bytes from {}", size, remote_address); 398 | tracker.handle_packet(&remote_address, &packet[..size]).await; 399 | }); 400 | } 401 | } 402 | } 403 | 404 | #[cfg(test)] 405 | mod tests { 406 | use super::*; 407 | #[test] 408 | fn pack() { 409 | let mystruct = super::UDPRequestHeader { 410 | connection_id: 0xc0c1c2c3c4c5c6c7, 411 | action: super::Actions::Connect, 412 | transaction_id: 77771, 413 | }; 414 | let mut buffer = [0u8; MAX_PACKET_SIZE]; 415 | let mut payload = StackVec::from(&mut buffer); 416 | 417 | assert!(pack_into(&mut payload, &mystruct).is_ok()); 418 | assert_eq!(payload.as_slice().len(), 16); 419 | assert_eq!(payload.as_slice(), &[ 420 | 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0, 0, 0, 0, 0, 1, 47, 203 421 | ]); 422 | } 423 | 424 | #[test] 425 | fn unpack() { 426 | let buf = [0xc0u8, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0, 0, 0, 1, 0, 1, 47, 203]; 427 | match super::unpack(&buf) { 428 | Some(obj) => { 429 | let x: super::UDPRequestHeader = obj; 430 | assert_eq!(x.connection_id, 0xc0c1c2c3c4c5c6c7); 431 | } 432 | None => { 433 | unreachable!(); 434 | } 435 | } 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /src/stackvec.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub struct StackVec<'a, T: 'a> { 4 | data: &'a mut [T], 5 | length: usize, 6 | } 7 | 8 | impl<'a, T> StackVec<'a, T> { 9 | pub fn from(data: &mut [T]) -> StackVec { 10 | StackVec { data, length: 0 } 11 | } 12 | 13 | pub fn as_slice(&self) -> &[T] { 14 | &self.data[0..self.length] 15 | } 16 | } 17 | 18 | impl<'a, T> Extend for StackVec<'a, T> { 19 | fn extend>(&mut self, iter: I) { 20 | for item in iter { 21 | self.data[self.length] = item; 22 | self.length += 1; 23 | } 24 | } 25 | } 26 | 27 | impl<'a> io::Write for StackVec<'a, u8> { 28 | fn write(&mut self, buf: &[u8]) -> io::Result { 29 | if buf.len() > (self.data.len() - self.length) { 30 | // not enough space on buffer. 31 | return Err(io::Error::from(io::ErrorKind::WriteZero)); 32 | } 33 | let writable = &mut self.data[self.length..][0..buf.len()]; 34 | writable.copy_from_slice(buf); 35 | self.length += buf.len(); 36 | Ok(buf.len()) 37 | } 38 | 39 | fn flush(&mut self) -> io::Result<()> { 40 | Ok(()) 41 | } 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | 48 | #[test] 49 | fn vec_write() { 50 | use std::io::Write; 51 | 52 | let mut buf = [0u8; 200]; 53 | { 54 | let mut vec = StackVec::from(&mut buf); 55 | assert!(vec.write("Hello World!".as_bytes()).is_ok()); 56 | } 57 | assert_eq!(buf[1] as char, 'e'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/tracker.rs: -------------------------------------------------------------------------------- 1 | use crate::server::Events; 2 | use log::{error, trace}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::borrow::Cow; 5 | use std::collections::BTreeMap; 6 | use tokio::io::AsyncBufReadExt; 7 | use tokio::sync::RwLock; 8 | 9 | const TWO_HOURS: std::time::Duration = std::time::Duration::from_secs(3600 * 2); 10 | 11 | #[derive(Deserialize, Clone, PartialEq)] 12 | pub enum TrackerMode { 13 | /// In static mode torrents are tracked only if they were added ahead of time. 14 | #[serde(rename = "static")] 15 | Static, 16 | 17 | /// In dynamic mode, torrents are tracked being added ahead of time. 18 | #[serde(rename = "dynamic")] 19 | Dynamic, 20 | 21 | /// Tracker will only serve authenticated peers. 22 | #[serde(rename = "private")] 23 | Private, 24 | } 25 | 26 | #[derive(Clone, Serialize)] 27 | pub struct TorrentPeer { 28 | ip: std::net::SocketAddr, 29 | uploaded: u64, 30 | downloaded: u64, 31 | left: u64, 32 | event: Events, 33 | #[serde(serialize_with = "ser_instant")] 34 | updated: std::time::Instant, 35 | } 36 | 37 | fn ser_instant(inst: &std::time::Instant, ser: S) -> Result { 38 | ser.serialize_u64(inst.elapsed().as_millis() as u64) 39 | } 40 | 41 | #[derive(Ord, PartialOrd, PartialEq, Eq, Clone)] 42 | pub struct InfoHash { 43 | info_hash: [u8; 20], 44 | } 45 | 46 | impl std::fmt::Display for InfoHash { 47 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 48 | let mut chars = [0u8; 40]; 49 | binascii::bin2hex(&self.info_hash, &mut chars).expect("failed to hexlify"); 50 | write!(f, "{}", std::str::from_utf8(&chars).unwrap()) 51 | } 52 | } 53 | 54 | impl std::str::FromStr for InfoHash { 55 | type Err = binascii::ConvertError; 56 | 57 | fn from_str(s: &str) -> Result { 58 | let mut i = Self { info_hash: [0u8; 20] }; 59 | if s.len() != 40 { 60 | return Err(binascii::ConvertError::InvalidInputLength); 61 | } 62 | binascii::hex2bin(s.as_bytes(), &mut i.info_hash)?; 63 | Ok(i) 64 | } 65 | } 66 | 67 | impl std::convert::From<&[u8]> for InfoHash { 68 | fn from(data: &[u8]) -> InfoHash { 69 | assert_eq!(data.len(), 20); 70 | let mut ret = InfoHash { info_hash: [0u8; 20] }; 71 | ret.info_hash.clone_from_slice(data); 72 | ret 73 | } 74 | } 75 | 76 | impl From<[u8; 20]> for InfoHash { 77 | fn from(info_hash: [u8; 20]) -> Self { 78 | InfoHash { info_hash } 79 | } 80 | } 81 | 82 | impl serde::ser::Serialize for InfoHash { 83 | fn serialize(&self, serializer: S) -> Result { 84 | let mut buffer = [0u8; 40]; 85 | let bytes_out = binascii::bin2hex(&self.info_hash, &mut buffer).ok().unwrap(); 86 | let str_out = std::str::from_utf8(bytes_out).unwrap(); 87 | 88 | serializer.serialize_str(str_out) 89 | } 90 | } 91 | 92 | struct InfoHashVisitor; 93 | 94 | impl<'v> serde::de::Visitor<'v> for InfoHashVisitor { 95 | type Value = InfoHash; 96 | 97 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 98 | write!(formatter, "a 40 character long hash") 99 | } 100 | 101 | fn visit_str(self, v: &str) -> Result { 102 | if v.len() != 40 { 103 | return Err(serde::de::Error::invalid_value( 104 | serde::de::Unexpected::Str(v), 105 | &"expected a 40 character long string", 106 | )); 107 | } 108 | 109 | let mut res = InfoHash { info_hash: [0u8; 20] }; 110 | 111 | if binascii::hex2bin(v.as_bytes(), &mut res.info_hash).is_err() { 112 | return Err(serde::de::Error::invalid_value( 113 | serde::de::Unexpected::Str(v), 114 | &"expected a hexadecimal string", 115 | )); 116 | } else { 117 | Ok(res) 118 | } 119 | } 120 | } 121 | 122 | impl<'de> serde::de::Deserialize<'de> for InfoHash { 123 | fn deserialize>(des: D) -> Result { 124 | des.deserialize_str(InfoHashVisitor) 125 | } 126 | } 127 | 128 | #[repr(transparent)] 129 | #[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq)] 130 | pub struct PeerId([u8; 20]); 131 | impl PeerId { 132 | pub fn from_array(v: &[u8; 20]) -> &PeerId { 133 | unsafe { 134 | // This is safe since PeerId's repr is transparent and content's are identical. PeerId == [0u8; 20] 135 | core::mem::transmute(v) 136 | } 137 | } 138 | 139 | pub fn get_client_name(&self) -> Option<&'static str> { 140 | if self.0[0] == b'M' { 141 | return Some("BitTorrent"); 142 | } 143 | if self.0[0] == b'-' { 144 | let name = match &self.0[1..3] { 145 | b"AG" => "Ares", 146 | b"A~" => "Ares", 147 | b"AR" => "Arctic", 148 | b"AV" => "Avicora", 149 | b"AX" => "BitPump", 150 | b"AZ" => "Azureus", 151 | b"BB" => "BitBuddy", 152 | b"BC" => "BitComet", 153 | b"BF" => "Bitflu", 154 | b"BG" => "BTG (uses Rasterbar libtorrent)", 155 | b"BR" => "BitRocket", 156 | b"BS" => "BTSlave", 157 | b"BX" => "~Bittorrent X", 158 | b"CD" => "Enhanced CTorrent", 159 | b"CT" => "CTorrent", 160 | b"DE" => "DelugeTorrent", 161 | b"DP" => "Propagate Data Client", 162 | b"EB" => "EBit", 163 | b"ES" => "electric sheep", 164 | b"FT" => "FoxTorrent", 165 | b"FW" => "FrostWire", 166 | b"FX" => "Freebox BitTorrent", 167 | b"GS" => "GSTorrent", 168 | b"HL" => "Halite", 169 | b"HN" => "Hydranode", 170 | b"KG" => "KGet", 171 | b"KT" => "KTorrent", 172 | b"LH" => "LH-ABC", 173 | b"LP" => "Lphant", 174 | b"LT" => "libtorrent", 175 | b"lt" => "libTorrent", 176 | b"LW" => "LimeWire", 177 | b"MO" => "MonoTorrent", 178 | b"MP" => "MooPolice", 179 | b"MR" => "Miro", 180 | b"MT" => "MoonlightTorrent", 181 | b"NX" => "Net Transport", 182 | b"PD" => "Pando", 183 | b"qB" => "qBittorrent", 184 | b"QD" => "QQDownload", 185 | b"QT" => "Qt 4 Torrent example", 186 | b"RT" => "Retriever", 187 | b"S~" => "Shareaza alpha/beta", 188 | b"SB" => "~Swiftbit", 189 | b"SS" => "SwarmScope", 190 | b"ST" => "SymTorrent", 191 | b"st" => "sharktorrent", 192 | b"SZ" => "Shareaza", 193 | b"TN" => "TorrentDotNET", 194 | b"TR" => "Transmission", 195 | b"TS" => "Torrentstorm", 196 | b"TT" => "TuoTu", 197 | b"UL" => "uLeecher!", 198 | b"UT" => "µTorrent", 199 | b"UW" => "µTorrent Web", 200 | b"VG" => "Vagaa", 201 | b"WD" => "WebTorrent Desktop", 202 | b"WT" => "BitLet", 203 | b"WW" => "WebTorrent", 204 | b"WY" => "FireTorrent", 205 | b"XL" => "Xunlei", 206 | b"XT" => "XanTorrent", 207 | b"XX" => "Xtorrent", 208 | b"ZT" => "ZipTorrent", 209 | _ => return None, 210 | }; 211 | Some(name) 212 | } else { 213 | None 214 | } 215 | } 216 | } 217 | impl Serialize for PeerId { 218 | fn serialize(&self, serializer: S) -> Result 219 | where 220 | S: serde::Serializer, { 221 | let mut tmp = [0u8; 40]; 222 | binascii::bin2hex(&self.0, &mut tmp).unwrap(); 223 | let id = std::str::from_utf8(&tmp).ok(); 224 | 225 | #[derive(Serialize)] 226 | struct PeerIdInfo<'a> { 227 | id: Option<&'a str>, 228 | client: Option<&'a str>, 229 | } 230 | 231 | let obj = PeerIdInfo { 232 | id, 233 | client: self.get_client_name(), 234 | }; 235 | obj.serialize(serializer) 236 | } 237 | } 238 | 239 | #[derive(Serialize, Deserialize, Clone)] 240 | pub struct TorrentEntry { 241 | is_flagged: bool, 242 | 243 | #[serde(skip)] 244 | peers: std::collections::BTreeMap, 245 | 246 | completed: u32, 247 | 248 | #[serde(skip)] 249 | seeders: u32, 250 | } 251 | 252 | impl TorrentEntry { 253 | pub fn new() -> TorrentEntry { 254 | TorrentEntry { 255 | is_flagged: false, 256 | peers: std::collections::BTreeMap::new(), 257 | completed: 0, 258 | seeders: 0, 259 | } 260 | } 261 | 262 | pub fn is_flagged(&self) -> bool { 263 | self.is_flagged 264 | } 265 | 266 | pub fn update_peer( 267 | &mut self, peer_id: &PeerId, remote_address: &std::net::SocketAddr, uploaded: u64, downloaded: u64, left: u64, 268 | event: Events, 269 | ) { 270 | let is_seeder = left == 0 && uploaded > 0; 271 | let mut was_seeder = false; 272 | let mut is_completed = left == 0 && (event as u32) == (Events::Complete as u32); 273 | if let Some(prev) = self.peers.insert(*peer_id, TorrentPeer { 274 | updated: std::time::Instant::now(), 275 | left, 276 | downloaded, 277 | uploaded, 278 | ip: *remote_address, 279 | event, 280 | }) { 281 | was_seeder = prev.left == 0 && prev.uploaded > 0; 282 | 283 | if is_completed && (prev.event as u32) == (Events::Complete as u32) { 284 | // don't update count again. a torrent should only be updated once per peer. 285 | is_completed = false; 286 | } 287 | } 288 | 289 | if is_seeder && !was_seeder { 290 | self.seeders += 1; 291 | } else if was_seeder && !is_seeder { 292 | self.seeders -= 1; 293 | } 294 | 295 | if is_completed { 296 | self.completed += 1; 297 | } 298 | } 299 | 300 | pub fn get_peers(&self, remote_addr: &std::net::SocketAddr) -> Vec { 301 | let mut list = Vec::new(); 302 | for (_, peer) in self 303 | .peers 304 | .iter() 305 | .filter(|e| e.1.ip.is_ipv4() == remote_addr.is_ipv4()) 306 | .take(74) 307 | { 308 | if peer.ip == *remote_addr { 309 | continue; 310 | } 311 | 312 | list.push(peer.ip); 313 | } 314 | list 315 | } 316 | 317 | pub fn get_peers_iter(&self) -> impl Iterator { 318 | self.peers.iter() 319 | } 320 | 321 | pub fn get_stats(&self) -> (u32, u32, u32) { 322 | let leechers = (self.peers.len() as u32) - self.seeders; 323 | (self.seeders, self.completed, leechers) 324 | } 325 | } 326 | 327 | struct TorrentDatabase { 328 | torrent_peers: tokio::sync::RwLock>, 329 | } 330 | 331 | impl Default for TorrentDatabase { 332 | fn default() -> Self { 333 | TorrentDatabase { 334 | torrent_peers: tokio::sync::RwLock::new(std::collections::BTreeMap::new()), 335 | } 336 | } 337 | } 338 | 339 | pub struct TorrentTracker { 340 | mode: TrackerMode, 341 | database: TorrentDatabase, 342 | } 343 | 344 | #[derive(Serialize, Deserialize)] 345 | struct DatabaseRow<'a> { 346 | info_hash: InfoHash, 347 | entry: Cow<'a, TorrentEntry>, 348 | } 349 | 350 | pub enum TorrentStats { 351 | TorrentFlagged, 352 | TorrentNotRegistered, 353 | Stats { seeders: u32, leechers: u32, complete: u32 }, 354 | } 355 | 356 | impl TorrentTracker { 357 | pub fn new(mode: TrackerMode) -> TorrentTracker { 358 | TorrentTracker { 359 | mode, 360 | database: TorrentDatabase { 361 | torrent_peers: RwLock::new(std::collections::BTreeMap::new()), 362 | }, 363 | } 364 | } 365 | 366 | pub async fn load_database( 367 | mode: TrackerMode, reader: &mut R, 368 | ) -> Result { 369 | let reader = tokio::io::BufReader::new(reader); 370 | let reader = async_compression::tokio::bufread::BzDecoder::new(reader); 371 | let reader = tokio::io::BufReader::new(reader); 372 | 373 | let res = TorrentTracker::new(mode); 374 | let mut db = res.database.torrent_peers.write().await; 375 | 376 | let mut records = reader.lines(); 377 | loop { 378 | let line = match records.next_line().await { 379 | Ok(Some(v)) => v, 380 | Ok(None) => break, 381 | Err(ref err) => { 382 | error!("failed to read lines! {}", err); 383 | continue; 384 | } 385 | }; 386 | let row: DatabaseRow = match serde_json::from_str(&line) { 387 | Ok(v) => v, 388 | Err(err) => { 389 | error!("failed to parse json: {}", err); 390 | continue; 391 | } 392 | }; 393 | let entry = row.entry.into_owned(); 394 | let infohash = row.info_hash; 395 | db.insert(infohash, entry); 396 | } 397 | 398 | trace!("loaded {} entries from database", db.len()); 399 | 400 | drop(db); 401 | 402 | Ok(res) 403 | } 404 | 405 | /// Adding torrents is not relevant to dynamic trackers. 406 | pub async fn add_torrent(&self, info_hash: &InfoHash) -> Result<(), ()> { 407 | let mut write_lock = self.database.torrent_peers.write().await; 408 | match write_lock.entry(info_hash.clone()) { 409 | std::collections::btree_map::Entry::Vacant(ve) => { 410 | ve.insert(TorrentEntry::new()); 411 | Ok(()) 412 | } 413 | std::collections::btree_map::Entry::Occupied(_entry) => Err(()), 414 | } 415 | } 416 | 417 | /// If the torrent is flagged, it will not be removed unless force is set to true. 418 | pub async fn remove_torrent(&self, info_hash: &InfoHash, force: bool) -> Result<(), ()> { 419 | use std::collections::btree_map::Entry; 420 | let mut entry_lock = self.database.torrent_peers.write().await; 421 | let torrent_entry = entry_lock.entry(info_hash.clone()); 422 | match torrent_entry { 423 | Entry::Vacant(_) => { 424 | // no entry, nothing to do... 425 | } 426 | Entry::Occupied(entry) => { 427 | if force || !entry.get().is_flagged() { 428 | entry.remove(); 429 | return Ok(()); 430 | } 431 | } 432 | } 433 | Err(()) 434 | } 435 | 436 | /// flagged torrents will result in a tracking error. This is to allow enforcement against piracy. 437 | pub async fn set_torrent_flag(&self, info_hash: &InfoHash, is_flagged: bool) -> bool { 438 | if let Some(entry) = self.database.torrent_peers.write().await.get_mut(info_hash) { 439 | if is_flagged && !entry.is_flagged { 440 | // empty peer list. 441 | entry.peers.clear(); 442 | } 443 | entry.is_flagged = is_flagged; 444 | true 445 | } else { 446 | false 447 | } 448 | } 449 | 450 | pub async fn get_torrent_peers( 451 | &self, info_hash: &InfoHash, remote_addr: &std::net::SocketAddr, 452 | ) -> Option> { 453 | let read_lock = self.database.torrent_peers.read().await; 454 | read_lock.get(info_hash).map(|entry| entry.get_peers(remote_addr)) 455 | } 456 | 457 | pub async fn update_torrent_and_get_stats( 458 | &self, info_hash: &InfoHash, peer_id: &PeerId, remote_address: &std::net::SocketAddr, uploaded: u64, 459 | downloaded: u64, left: u64, event: Events, 460 | ) -> TorrentStats { 461 | use std::collections::btree_map::Entry; 462 | let mut torrent_peers = self.database.torrent_peers.write().await; 463 | let torrent_entry = match torrent_peers.entry(info_hash.clone()) { 464 | Entry::Vacant(vacant) => { 465 | match self.mode { 466 | TrackerMode::Dynamic => vacant.insert(TorrentEntry::new()), 467 | _ => { 468 | return TorrentStats::TorrentNotRegistered; 469 | } 470 | } 471 | } 472 | Entry::Occupied(entry) => { 473 | if entry.get().is_flagged() { 474 | return TorrentStats::TorrentFlagged; 475 | } 476 | entry.into_mut() 477 | } 478 | }; 479 | 480 | torrent_entry.update_peer(peer_id, remote_address, uploaded, downloaded, left, event); 481 | 482 | let (seeders, complete, leechers) = torrent_entry.get_stats(); 483 | 484 | TorrentStats::Stats { 485 | seeders, 486 | leechers, 487 | complete, 488 | } 489 | } 490 | 491 | pub(crate) async fn get_database(&self) -> tokio::sync::RwLockReadGuard<'_, BTreeMap> { 492 | self.database.torrent_peers.read().await 493 | } 494 | 495 | pub async fn save_database(&self, w: W) -> Result<(), std::io::Error> { 496 | use tokio::io::AsyncWriteExt; 497 | 498 | let mut writer = async_compression::tokio::write::BzEncoder::new(w); 499 | 500 | let db_lock = self.database.torrent_peers.read().await; 501 | 502 | let db: &BTreeMap = &db_lock; 503 | let mut tmp = Vec::with_capacity(4096); 504 | 505 | for row in db { 506 | let entry = DatabaseRow { 507 | info_hash: row.0.clone(), 508 | entry: Cow::Borrowed(row.1), 509 | }; 510 | tmp.clear(); 511 | if let Err(err) = serde_json::to_writer(&mut tmp, &entry) { 512 | error!("failed to serialize: {}", err); 513 | continue; 514 | }; 515 | tmp.push(b'\n'); 516 | writer.write_all(&tmp).await?; 517 | } 518 | writer.flush().await?; 519 | Ok(()) 520 | } 521 | 522 | async fn cleanup(&self) { 523 | let mut lock = self.database.torrent_peers.write().await; 524 | let db: &mut BTreeMap = &mut lock; 525 | let mut torrents_to_remove = Vec::new(); 526 | 527 | for (k, v) in db.iter_mut() { 528 | // timed-out peers.. 529 | { 530 | let mut peers_to_remove = Vec::new(); 531 | let torrent_peers = &mut v.peers; 532 | 533 | for (peer_id, state) in torrent_peers.iter() { 534 | if state.updated.elapsed() > TWO_HOURS { 535 | // over 2 hours past since last update... 536 | peers_to_remove.push(*peer_id); 537 | } 538 | } 539 | 540 | for peer_id in peers_to_remove.iter() { 541 | torrent_peers.remove(peer_id); 542 | } 543 | } 544 | 545 | if self.mode == TrackerMode::Dynamic { 546 | // peer-less torrents.. 547 | if v.peers.is_empty() && !v.is_flagged() { 548 | torrents_to_remove.push(k.clone()); 549 | } 550 | } 551 | } 552 | 553 | for info_hash in torrents_to_remove { 554 | db.remove(&info_hash); 555 | } 556 | } 557 | 558 | pub async fn periodic_task(&self, db_path: &str) { 559 | // cleanup db 560 | self.cleanup().await; 561 | 562 | // save journal db. 563 | let mut journal_path = std::path::PathBuf::from(db_path); 564 | 565 | let mut filename = String::from(journal_path.file_name().unwrap().to_str().unwrap()); 566 | filename.push_str("-journal"); 567 | 568 | journal_path.set_file_name(filename.as_str()); 569 | let jp_str = journal_path.as_path().to_str().unwrap(); 570 | 571 | // scope to make sure backup file is dropped/closed. 572 | { 573 | let mut file = match tokio::fs::File::create(jp_str).await { 574 | Err(err) => { 575 | error!("failed to open file '{}': {}", db_path, err); 576 | return; 577 | } 578 | Ok(v) => v, 579 | }; 580 | trace!("writing database to {}", jp_str); 581 | if let Err(err) = self.save_database(&mut file).await { 582 | error!("failed saving database. {}", err); 583 | return; 584 | } 585 | } 586 | 587 | // overwrite previous db 588 | trace!("renaming '{}' to '{}'", jp_str, db_path); 589 | if let Err(err) = tokio::fs::rename(jp_str, db_path).await { 590 | error!("failed to move db backup. {}", err); 591 | } 592 | } 593 | } 594 | 595 | #[cfg(test)] 596 | mod tests { 597 | use super::*; 598 | 599 | fn is_sync() {} 600 | fn is_send() {} 601 | 602 | #[test] 603 | fn tracker_send() { 604 | is_send::(); 605 | } 606 | 607 | #[test] 608 | fn tracker_sync() { 609 | is_sync::(); 610 | } 611 | 612 | #[tokio::test] 613 | async fn test_save_db() { 614 | let tracker = TorrentTracker::new(TrackerMode::Dynamic); 615 | tracker 616 | .add_torrent(&[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0].into()) 617 | .await 618 | .expect("failed to add torrent"); 619 | 620 | let mut out = Vec::new(); 621 | let mut cursor = std::io::Cursor::new(&mut out); 622 | 623 | tracker.save_database(&mut cursor).await.expect("db save failed"); 624 | assert!(cursor.position() > 0); 625 | } 626 | 627 | #[test] 628 | fn test_infohash_de() { 629 | use serde_json; 630 | 631 | let ih: InfoHash = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1].into(); 632 | 633 | let serialized_ih = serde_json::to_string(&ih).unwrap(); 634 | 635 | let de_ih: InfoHash = serde_json::from_str(serialized_ih.as_str()).unwrap(); 636 | 637 | assert!(de_ih == ih); 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /src/webserver.rs: -------------------------------------------------------------------------------- 1 | use crate::tracker::{InfoHash, TorrentTracker}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::cmp::min; 4 | use std::collections::{HashMap, HashSet}; 5 | use std::sync::Arc; 6 | use warp::{filters, reply, reply::Reply, serve, Filter, Server}; 7 | 8 | fn view_root() -> impl Reply { 9 | warp::http::Response::builder() 10 | .header("Content-Type", "text/html; charset=utf-8") 11 | .header("Server", concat!("udpt/", env!("CARGO_PKG_VERSION"), "; https://abda.nl/")) 12 | .body(concat!(r#" 13 | 14 | udpt server 15 | 28 | 29 | 30 |

31 | This server is running udpt, a BitTorrent tracker based on the UDP protocol. 32 |

33 |
34 | udpt/"#, env!("CARGO_PKG_VERSION"), r#"
35 | docs · issues & PRs · donate 36 |
37 | 38 | "#)) 39 | .unwrap() 40 | } 41 | 42 | #[derive(Deserialize, Debug)] 43 | struct TorrentInfoQuery { 44 | offset: Option, 45 | limit: Option, 46 | } 47 | 48 | #[derive(Serialize)] 49 | struct TorrentEntry<'a> { 50 | info_hash: &'a InfoHash, 51 | #[serde(flatten)] 52 | data: &'a crate::tracker::TorrentEntry, 53 | seeders: u32, 54 | leechers: u32, 55 | 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | peers: Option>, 58 | } 59 | 60 | #[derive(Serialize, Deserialize)] 61 | struct TorrentFlag { 62 | is_flagged: bool, 63 | } 64 | 65 | #[derive(Serialize, Debug)] 66 | #[serde(tag = "status", rename_all = "snake_case")] 67 | enum ActionStatus<'a> { 68 | Ok, 69 | Err { reason: std::borrow::Cow<'a, str> }, 70 | } 71 | 72 | impl warp::reject::Reject for ActionStatus<'static> {} 73 | 74 | fn authenticate(tokens: HashMap) -> impl Filter + Clone { 75 | #[derive(Deserialize)] 76 | struct AuthToken { 77 | token: Option, 78 | } 79 | 80 | let tokens: HashSet = tokens.into_values().collect(); 81 | 82 | let tokens = Arc::new(tokens); 83 | warp::filters::any::any() 84 | .map(move || tokens.clone()) 85 | .and(filters::query::query::()) 86 | .and_then(|tokens: Arc>, token: AuthToken| { 87 | async move { 88 | if let Some(token) = token.token { 89 | if tokens.contains(&token) { 90 | return Ok(()); 91 | } 92 | } 93 | Err(warp::reject::custom(ActionStatus::Err { 94 | reason: "Access Denied".into(), 95 | })) 96 | } 97 | }) 98 | .untuple_one() 99 | } 100 | 101 | pub fn build_server( 102 | tracker: Arc, tokens: HashMap, 103 | ) -> Server + Clone + Send + Sync + 'static> { 104 | let root = filters::path::end().map(view_root); 105 | 106 | let t1 = tracker.clone(); 107 | // view_torrent_list -> GET /t/?offset=:u32&limit=:u32 HTTP/1.1 108 | let view_torrent_list = filters::path::end() 109 | .and(filters::method::get()) 110 | .and(filters::query::query()) 111 | .map(move |limits| { 112 | let tracker = t1.clone(); 113 | (limits, tracker) 114 | }) 115 | .and_then(|(limits, tracker): (TorrentInfoQuery, Arc)| { 116 | async move { 117 | let offset = limits.offset.unwrap_or(0); 118 | let limit = min(limits.limit.unwrap_or(1000), 4000); 119 | 120 | let db = tracker.get_database().await; 121 | let results: Vec<_> = db 122 | .iter() 123 | .map(|(k, v)| { 124 | let (seeders, _, leechers) = v.get_stats(); 125 | TorrentEntry { 126 | info_hash: k, 127 | data: v, 128 | seeders, 129 | leechers, 130 | peers: None, 131 | } 132 | }) 133 | .skip(offset as usize) 134 | .take(limit as usize) 135 | .collect(); 136 | 137 | Result::<_, warp::reject::Rejection>::Ok(reply::json(&results)) 138 | } 139 | }); 140 | 141 | let t2 = tracker.clone(); 142 | // view_torrent_info -> GET /t/:infohash HTTP/* 143 | let view_torrent_info = filters::method::get() 144 | .and(filters::path::param()) 145 | .and(filters::path::end()) 146 | .map(move |info_hash: InfoHash| { 147 | let tracker = t2.clone(); 148 | (info_hash, tracker) 149 | }) 150 | .and_then(|(info_hash, tracker): (InfoHash, Arc)| { 151 | async move { 152 | let db = tracker.get_database().await; 153 | let info = match db.get(&info_hash) { 154 | Some(v) => v, 155 | None => return Err(warp::reject::reject()), 156 | }; 157 | let (seeders, _, leechers) = info.get_stats(); 158 | 159 | let peers: Vec<_> = info 160 | .get_peers_iter() 161 | .take(1000) 162 | .map(|(&peer_id, peer_info)| (peer_id, peer_info.clone())) 163 | .collect(); 164 | 165 | Ok(reply::json(&TorrentEntry { 166 | info_hash: &info_hash, 167 | data: info, 168 | seeders, 169 | leechers, 170 | peers: Some(peers), 171 | })) 172 | } 173 | }); 174 | 175 | // DELETE /t/:info_hash 176 | let t3 = tracker.clone(); 177 | let delete_torrent = filters::method::delete() 178 | .and(filters::path::param()) 179 | .and(filters::path::end()) 180 | .map(move |info_hash: InfoHash| { 181 | let tracker = t3.clone(); 182 | (info_hash, tracker) 183 | }) 184 | .and_then(|(info_hash, tracker): (InfoHash, Arc)| { 185 | async move { 186 | let resp = match tracker.remove_torrent(&info_hash, true).await.is_ok() { 187 | true => ActionStatus::Ok, 188 | false => { 189 | ActionStatus::Err { 190 | reason: "failed to delete torrent".into(), 191 | } 192 | } 193 | }; 194 | 195 | Result::<_, warp::Rejection>::Ok(reply::json(&resp)) 196 | } 197 | }); 198 | 199 | let t4 = tracker; 200 | // add_torrent/alter: POST /t/:info_hash 201 | // (optional) BODY: json: {"is_flagged": boolean} 202 | let change_torrent = filters::method::post() 203 | .and(filters::path::param()) 204 | .and(filters::path::end()) 205 | .and(filters::body::content_length_limit(4096)) 206 | .and(filters::body::json()) 207 | .map(move |info_hash: InfoHash, body: Option| { 208 | let tracker = t4.clone(); 209 | (info_hash, tracker, body) 210 | }) 211 | .and_then( 212 | |(info_hash, tracker, body): (InfoHash, Arc, Option)| { 213 | async move { 214 | let is_flagged = body.map(|e| e.is_flagged).unwrap_or(false); 215 | if !tracker.set_torrent_flag(&info_hash, is_flagged).await { 216 | // torrent doesn't exist, add it... 217 | 218 | if is_flagged { 219 | if tracker.add_torrent(&info_hash).await.is_ok() { 220 | tracker.set_torrent_flag(&info_hash, is_flagged).await; 221 | } else { 222 | return Err(warp::reject::custom(ActionStatus::Err { 223 | reason: "failed to flag torrent".into(), 224 | })); 225 | } 226 | } 227 | } 228 | 229 | Result::<_, warp::Rejection>::Ok(reply::json(&ActionStatus::Ok)) 230 | } 231 | }, 232 | ); 233 | let torrent_mgmt = 234 | filters::path::path("t").and(view_torrent_list.or(delete_torrent).or(view_torrent_info).or(change_torrent)); 235 | 236 | let server = root.or(authenticate(tokens).and(torrent_mgmt)); 237 | 238 | serve(server) 239 | } 240 | -------------------------------------------------------------------------------- /udpt.toml: -------------------------------------------------------------------------------- 1 | mode = "dynamic" 2 | db_path = "database.json.bz2" 3 | log_level = "trace" 4 | 5 | [udp] 6 | announce_interval = 120 # Two minutes 7 | bind_address = "0.0.0.0:1212" 8 | 9 | [http] 10 | bind_address = "127.0.0.1:1212" 11 | 12 | [http.access_tokens] 13 | someone = "MyAccessToken" 14 | --------------------------------------------------------------------------------