├── .actrc ├── .cargo └── config.toml ├── .envrc ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── vcz │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── vcz_daemon │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── vcz_ui │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── action.rs │ │ ├── app.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── pages │ │ ├── mod.rs │ │ └── torrent_list.rs │ │ └── tui.rs └── vincenzo │ ├── Cargo.toml │ ├── README.md │ ├── src │ ├── args.rs │ ├── avg.rs │ ├── bitfield.rs │ ├── config.rs │ ├── counter.rs │ ├── daemon.rs │ ├── daemon_wire │ │ └── mod.rs │ ├── disk.rs │ ├── error.rs │ ├── extensions │ │ ├── core │ │ │ ├── codec.rs │ │ │ ├── handshake_codec.rs │ │ │ ├── message.rs │ │ │ └── mod.rs │ │ ├── extended │ │ │ ├── codec.rs │ │ │ ├── mod.rs │ │ │ └── trait.rs │ │ ├── metadata │ │ │ ├── codec.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── lib.rs │ ├── magnet.rs │ ├── metainfo.rs │ ├── peer │ │ ├── mod.rs │ │ └── session.rs │ ├── torrent.rs │ ├── tracker │ │ ├── action.rs │ │ ├── announce.rs │ │ ├── connect.rs │ │ ├── event.rs │ │ └── mod.rs │ └── utils.rs │ ├── tape.gif │ └── tests │ └── integration.rs ├── dockerfile ├── flake.lock ├── flake.nix ├── rustfmt.toml ├── tape.gif ├── tape.tape └── test-files ├── book.torrent ├── debian.torrent ├── foo.txt ├── music.torrent └── pieces.iso /.actrc: -------------------------------------------------------------------------------- 1 | -P ubuntu-latest=gabrieldemian/vincenzo:latest 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "tokio_unstable"] 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gabrieldemian] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: vincenzo44 # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: [glombardo.dev/sponsor] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | - cron: '00 01 * * *' 9 | 10 | jobs: 11 | check: 12 | name: Check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v4 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: stable 23 | override: true 24 | 25 | - uses: Swatinem/rust-cache@v1 26 | 27 | - name: Run cargo check 28 | uses: actions-rs/cargo@v1 29 | with: 30 | command: check 31 | 32 | test: 33 | name: Test Suite 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest, macos-latest, windows-latest] 37 | rust: [stable] 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - name: Checkout sources 41 | uses: actions/checkout@v4 42 | 43 | - name: Install stable toolchain 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: ${{ matrix.rust }} 48 | override: true 49 | 50 | - uses: Swatinem/rust-cache@v1 51 | 52 | - name: Run cargo test 53 | uses: actions-rs/cargo@v1 54 | with: 55 | command: test 56 | 57 | 58 | lints: 59 | name: Lints 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout sources 63 | uses: actions/checkout@v2 64 | with: 65 | submodules: true 66 | 67 | - name: Install stable toolchain 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | profile: minimal 71 | toolchain: stable 72 | override: true 73 | components: rustfmt, clippy 74 | 75 | - uses: Swatinem/rust-cache@v1 76 | 77 | - name: Run cargo fmt 78 | uses: actions-rs/cargo@v1 79 | with: 80 | command: fmt 81 | args: --all -- --check 82 | 83 | - name: Run cargo clippy 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: clippy 87 | args: -- -D warnings 88 | 89 | - name: Run rustdoc lints 90 | uses: actions-rs/cargo@v1 91 | env: 92 | RUSTDOCFLAGS: "-D missing_docs -D rustdoc::missing_doc_code_examples" 93 | with: 94 | command: doc 95 | args: --workspace --all-features --no-deps --document-private-items 96 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | # (optional) Path to changelog. 19 | # changelog: CHANGELOG.md 20 | # (required) GitHub token for creating GitHub Releases. 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | upload-assets: 24 | needs: create-release 25 | strategy: 26 | matrix: 27 | include: 28 | - target: aarch64-unknown-linux-gnu 29 | os: ubuntu-latest 30 | - target: aarch64-apple-darwin 31 | os: macos-latest 32 | - target: x86_64-unknown-linux-gnu 33 | os: ubuntu-latest 34 | - target: x86_64-apple-darwin 35 | os: macos-latest 36 | - target: x86_64-unknown-freebsd 37 | os: ubuntu-latest 38 | - target: x86_64-pc-windows-msvc 39 | os: windows-latest 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Install cross-compilation tools 44 | uses: taiki-e/setup-cross-toolchain-action@v1 45 | with: 46 | target: ${{ matrix.target }} 47 | if: startsWith(matrix.os, 'ubuntu') 48 | - uses: taiki-e/upload-rust-binary-action@v1 49 | with: 50 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 51 | # Note that glob pattern is not supported yet. 52 | bin: vcz,vcz_ui,vczd 53 | archive: vcz-$target 54 | # (optional) Target triple, default is host triple. 55 | target: ${{ matrix.target }} 56 | # (required) GitHub token for uploading assets to GitHub Releases. 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | log.txt 4 | perf.data 5 | flamegraph.svg 6 | **.DS_Store 7 | /bin 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/vcz_daemon", 4 | "crates/vcz_ui", 5 | "crates/vincenzo", 6 | "crates/vcz", 7 | ] 8 | 9 | resolver = "2" 10 | 11 | [workspace.dependencies] 12 | bendy = { version = "0.3.3", features = ["std"] } 13 | bitvec = "1.0.1" 14 | bytes = "1.4.0" 15 | clap = { version = "4.3.4", features = ["derive"] } 16 | config = "0.14.0" 17 | crossterm = { version = "0.27.0", features = ["event-stream", "serde"] } 18 | directories = "5.0.1" 19 | futures = "0.3.28" 20 | hashbrown = "0.14.5" 21 | hex = "0.4.3" 22 | magnet-url = "2.0.0" 23 | rand = "0.8.5" 24 | ratatui = { version = "0.28.0", features = ["all-widgets"] } 25 | serde = { version = "1.0.185", features = ["derive"] } 26 | sha1_smol = { version = "1.0.0", features = ["serde"] } 27 | speedy = "0.8.6" 28 | thiserror = "1.0.47" 29 | time = "0.3.36" 30 | tokio = { version = "1.32.0", features = [ 31 | "rt", 32 | "fs", 33 | "tracing", 34 | "time", 35 | "macros", 36 | "rt-multi-thread", 37 | "sync", 38 | "io-std", 39 | "io-util", 40 | "net", 41 | ] } 42 | tokio-util = { version = "0.7.8", features = ["codec"] } 43 | toml = "0.8.0" 44 | tracing = "0.1.37" 45 | tracing-appender = "0.2.2" 46 | tracing-subscriber = { version = "0.3.17", features = ["time"] } 47 | urlencoding = "2.1.3" 48 | vcz = { path = "crates/vcz" } 49 | vcz_daemon = { path = "crates/vcz_daemon" } 50 | vcz_ui = { path = "crates/vcz_ui" } 51 | vincenzo = { path = "crates/vincenzo" } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gabriel Lombardo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vincenzo 2 | Vincenzo is a BitTorrent client with vim-like keybindings and a terminal based UI. 3 | 4 | [![Latest Version](https://img.shields.io/crates/v/vincenzo.svg)](https://crates.io/crates/vincenzo) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 5 | 6 | ![image](tape.gif) 7 | 8 | ## Introduction 9 | Vincenzo aims to be a fast, lightweight, and multi-platform client. 10 | 11 | Another goal is for users to be able to use the [library](crates/vincenzo) to create any other kind of software, that is powered by the BitTorrent protocol. 12 | 13 | The official UI binary is very niched, targeting a very specific type of user: someone who loves using the terminal, and vim keybindings. Altough users could create other UIs using the library. 14 | 15 | Vincenzo offers 3 binaries and 1 library: 16 | 17 | - [vcz](crates/vcz) - Main binary with both UI and daemon. 18 | - [vcz_ui](crates/vcz_ui) - UI binary. 19 | - [vczd](crates/vcz_daemon) - Daemon binary. 20 | - [vincenzo](crates/vincenzo) - Library 21 | 22 | ## Features 23 | - Multi-platform. 24 | - Multithreaded. One OS thread specific for I/O. 25 | - Async I/O with tokio. 26 | - Communication with daemon using CLI flags, TCP messages or remotely via UI binary. 27 | - Detached daemon from the UI. 28 | - Support for magnet links. 29 | 30 | ## How to use 31 | Downloading a torrent using the main binary (the flags are optional and could be omitted in favour of the configuration file). 32 | 33 | ```bash 34 | vcz -d "/tmp/download_dir" -m "" -q 35 | ``` 36 | 37 | ## Configuration 38 | The binaries read a toml config file. 39 | It is located at the default config folder of your OS. 40 | - Linux: ~/.config/vincenzo/config.toml 41 | - Windows: C:\Users\Alice\AppData\Roaming\Vincenzo\config.toml 42 | - MacOS: /Users/Alice/Library/Application Support/Vincenzo/config.toml 43 | 44 | ### Default config file: 45 | ```toml 46 | download_dir = "/home/alice/Downloads" 47 | # default 48 | daemon_addr = "127.0.0.1:3030" 49 | ``` 50 | 51 | ## Daemon and UI binaries 52 | Users can control the Daemon by using CLI flags that work as messages. 53 | 54 | Let's say on one terminal you initiate the daemon: `vczd`. Or spawn as a background process so you can do everything on one terminal: `vczd &`. 55 | 56 | And you open a second terminal to send messages to the daemon, add a torrent: `vczd -m "magnet:..."` and then print the stats to stdout `vczd --stats`. 57 | 58 | You can also run the UI binary (maybe remotely from another machine) to control the Daemon: `vcz_ui --daemon-addr 127.0.0.1:3030`. 59 | 60 |
61 | CLI flags of Daemon 62 | 63 | ``` 64 | Usage: vczd [OPTIONS] 65 | 66 | Options: 67 | --daemon-addr The Daemon will accept TCP connections on this address 68 | -d, --download-dir The directory in which torrents will be downloaded 69 | -m, --magnet Download a torrent using it's magnet link, wrapped in quotes 70 | -q, --quit-after-complete If the program should quit after all torrents are fully downloaded 71 | -s, --stats Print all torrent status on stdout 72 | -h, --help Print help 73 | -V, --version Print version 74 | ``` 75 |
76 | 77 | ## Supported BEPs 78 | - [BEP 0003](http://www.bittorrent.org/beps/bep_0003.html) - The BitTorrent Protocol Specification 79 | - [BEP 0009](http://www.bittorrent.org/beps/bep_0009.html) - Extension for Peers to Send Metadata Files 80 | - [BEP 0010](http://www.bittorrent.org/beps/bep_0010.html) - Extension Protocol 81 | - [BEP 0015](http://www.bittorrent.org/beps/bep_0015.html) - UDP Tracker Protocol 82 | - [BEP 0023](http://www.bittorrent.org/beps/bep_0023.html) - Tracker Returns Compact Peer Lists 83 | 84 | ## Roadmap 85 | - [x] Initial version of UI.
86 | - [x] Download pipelining.
87 | - [x] Endgame mode.
88 | - [x] Pause and resume torrents.
89 | - [x] Separate main binary into 3 binaries and 1 library.
90 | - [x] Cache bytes to reduce the number of writes on disk.
91 | - [x] Change piece selection strategy.
92 | - [ ] Choking algorithm.
93 | - [ ] Anti-snubbing.
94 | - [ ] Resume torrent download from a file.
95 | - [ ] Select files to download.
96 | - [ ] Support streaming of videos/music on MPV.
97 | 98 | ## Donations 99 | I'm working on this alone, if you enjoy my work, please consider a donation [here](https://www.glombardo.dev/sponsor). 100 | 101 | -------------------------------------------------------------------------------- /crates/vcz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz" 3 | version = "0.0.3" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "vcz" 8 | path = "src/main.rs" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | bendy = { workspace = true } 14 | bytes = { workspace = true } 15 | clap = { workspace = true } 16 | crossterm = { workspace = true } 17 | directories = { workspace = true } 18 | futures = { workspace = true } 19 | hashbrown = { workspace = true } 20 | hex = { workspace = true } 21 | magnet-url = { workspace = true } 22 | rand = { workspace = true } 23 | ratatui = { workspace = true } 24 | serde = { workspace = true } 25 | sha1_smol = { workspace = true } 26 | speedy = { workspace = true } 27 | thiserror = { workspace = true } 28 | tokio = { workspace = true } 29 | tokio-util = { workspace = true } 30 | toml = { workspace = true } 31 | tracing = { workspace = true } 32 | tracing-subscriber = { workspace = true } 33 | urlencoding = { workspace = true } 34 | tracing-appender = { workspace = true } 35 | time = { workspace = true } 36 | vincenzo = { workspace = true } 37 | vcz_ui = { workspace = true } 38 | -------------------------------------------------------------------------------- /crates/vcz/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This is the main binary of the client. This binary spawns both the Daemon and 3 | the UI at the same process. When the user closes the UI, the Daemon will be killed. 4 | 5 | ```bash 6 | vcz -d "/home/user/Downloads" -m "" -q 7 | ``` 8 | -------------------------------------------------------------------------------- /crates/vcz/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use tokio::join; 3 | use tracing::Level; 4 | use tracing_appender::rolling::{RollingFileAppender, Rotation}; 5 | use tracing_subscriber::{fmt::time::OffsetTime, FmtSubscriber}; 6 | use vincenzo::{args::Args, daemon::Daemon}; 7 | 8 | use vcz_ui::{action::Action, app::App}; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<(), Box> { 12 | let tmp = std::env::temp_dir(); 13 | let time = std::time::SystemTime::now(); 14 | let timestamp = 15 | time.duration_since(std::time::UNIX_EPOCH).unwrap().as_millis(); 16 | 17 | let file_appender = RollingFileAppender::new( 18 | Rotation::NEVER, 19 | tmp, 20 | format!("vcz-{timestamp}.log"), 21 | ); 22 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 23 | 24 | let subscriber = FmtSubscriber::builder() 25 | .with_max_level(Level::DEBUG) 26 | .with_writer(non_blocking) 27 | .with_timer(OffsetTime::new( 28 | time::UtcOffset::current_local_offset() 29 | .unwrap_or(time::UtcOffset::UTC), 30 | time::format_description::parse( 31 | "[year]-[month]-[day] [hour]:[minute]:[second]", 32 | ) 33 | .unwrap(), 34 | )) 35 | .with_ansi(false) 36 | .finish(); 37 | 38 | tracing::subscriber::set_global_default(subscriber) 39 | .expect("setting default subscriber failed"); 40 | 41 | let mut daemon = Daemon::new(); 42 | 43 | // Start and run the terminal UI 44 | let mut fr = App::new(); 45 | let fr_tx = fr.tx.clone(); 46 | 47 | let args = Args::parse(); 48 | 49 | // If the user passed a magnet through the CLI, 50 | // start this torrent immediately 51 | if let Some(magnet) = args.magnet { 52 | fr_tx.send(Action::NewTorrent(magnet)).unwrap(); 53 | } 54 | 55 | let (v1, v2) = join!(daemon.run(), fr.run()); 56 | v1?; 57 | v2?; 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /crates/vcz_daemon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz_daemon" 3 | version = "0.0.3" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "vczd" 8 | path = "src/main.rs" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | bendy = { workspace = true } 14 | bytes = { workspace = true } 15 | clap = { workspace = true } 16 | directories = { workspace = true } 17 | futures = { workspace = true } 18 | hashbrown = { workspace = true } 19 | hex = { workspace = true } 20 | magnet-url = { workspace = true } 21 | rand = { workspace = true } 22 | serde = { workspace = true } 23 | sha1_smol = { workspace = true } 24 | speedy = { workspace = true } 25 | thiserror = { workspace = true } 26 | tokio = { workspace = true } 27 | tokio-util = { workspace = true } 28 | toml = { workspace = true } 29 | tracing = { workspace = true } 30 | tracing-subscriber = { workspace = true } 31 | urlencoding = { workspace = true } 32 | vincenzo = { workspace = true } 33 | -------------------------------------------------------------------------------- /crates/vcz_daemon/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This is the binary of the daemon. A daemon is simply the "backend" that is responsible 3 | for adding torrents and writing/reading data on disk, communicating with the UI, 4 | and writing logs. 5 | 6 | The daemon listen on a TCP address that can be set on the configuration file, 7 | or through the CLI flag `--listen`. The default address is: `127.0.0.1:3030`. 8 | 9 | You can communicate with the Daemon in 2 days: 10 | - CLI flags 11 | - TCP messages 12 | 13 | The documentation can be found on the library crate. 14 | 15 | You need to have a download_dir on the config file or through the CLI, 16 | having none of these will error. 17 | 18 | ```bash 19 | vczd -d "/home/user/Downloads" -m "" 20 | ``` 21 | 22 | Adding torrents after the daemon is running: 23 | ```bash 24 | vczd -m "" 25 | ``` 26 | -------------------------------------------------------------------------------- /crates/vcz_daemon/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use futures::SinkExt; 3 | use tokio::net::{TcpListener, TcpStream}; 4 | use tokio_util::codec::Framed; 5 | use tracing::Level; 6 | use tracing_subscriber::FmtSubscriber; 7 | use vincenzo::{ 8 | args::Args, 9 | config::Config, 10 | daemon::Daemon, 11 | daemon_wire::{DaemonCodec, Message}, 12 | }; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | let args = Args::parse(); 17 | let config = Config::load()?; 18 | 19 | let daemon_addr = config.daemon_addr; 20 | 21 | let is_daemon_running = TcpListener::bind(daemon_addr).await.is_err(); 22 | 23 | // if the daemon is not running, run it 24 | if !is_daemon_running { 25 | let subscriber = FmtSubscriber::builder() 26 | .with_max_level(Level::INFO) 27 | .without_time() 28 | .finish(); 29 | 30 | tracing::subscriber::set_global_default(subscriber) 31 | .expect("setting default subscriber failed"); 32 | 33 | let mut daemon = Daemon::new(); 34 | 35 | daemon.run().await?; 36 | } 37 | 38 | // Now that the daemon is running on a process, 39 | // the user can send commands using CLI flags, 40 | // using a different terminal, and we want 41 | // to listen to these flags and send messages to Daemon. 42 | // 43 | // 1. Create a TCP connection to Daemon 44 | let socket = TcpStream::connect(daemon_addr).await?; 45 | 46 | let mut socket = Framed::new(socket, DaemonCodec); 47 | 48 | // 2. Fire the corresponding message of a CLI flag. 49 | // 50 | // add a a new torrent to Daemon 51 | if let Some(magnet) = args.magnet { 52 | socket.send(Message::NewTorrent(magnet)).await?; 53 | } 54 | 55 | if args.stats { 56 | socket.send(Message::PrintTorrentStatus).await?; 57 | } 58 | 59 | if args.quit { 60 | socket.send(Message::Quit).await?; 61 | } 62 | 63 | if let Some(id) = args.pause { 64 | let id = hex::decode(id); 65 | if let Ok(id) = id { 66 | socket.send(Message::TogglePause(id.try_into().unwrap())).await?; 67 | } 68 | } 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /crates/vcz_ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vcz_ui" 3 | version = "0.0.3" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { workspace = true } 10 | crossterm = { workspace = true } 11 | futures = { workspace = true } 12 | hashbrown = { workspace = true } 13 | magnet-url = { workspace = true } 14 | rand = { workspace = true } 15 | ratatui = { workspace = true } 16 | serde = { workspace = true } 17 | thiserror = { workspace = true } 18 | tokio = { workspace = true } 19 | tokio-util = { workspace = true } 20 | tracing = { workspace = true } 21 | vincenzo = { workspace = true } 22 | -------------------------------------------------------------------------------- /crates/vcz_ui/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is the official UI of Vincenzo, this is not published to crates.io. 4 | 5 | If you are using the UI, you must have the daemon running beforehand, 6 | otherwise it will error. 7 | 8 | If you want to run both the UI and the daemon at the same time, 9 | to go the main crate `vcz`. 10 | 11 | To use this binary, you need to know the address in which the daemon is running, 12 | you can pass that on the config file or through CLI flags. 13 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/action.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::KeyEvent; 2 | use vincenzo::torrent::TorrentState; 3 | 4 | /// A new component to be rendered on the UI. 5 | /// Used in conjunction with [`Action`] 6 | #[derive(Clone, Copy)] 7 | pub enum Page { 8 | // first page to be rendered 9 | TorrentList, 10 | // Details, 11 | } 12 | 13 | #[derive(Clone)] 14 | pub enum Action { 15 | Tick, 16 | Key(KeyEvent), 17 | Quit, 18 | Render, 19 | None, 20 | 21 | /// Render another page on the UI 22 | ChangePage(Page), 23 | 24 | NewTorrent(String), 25 | TogglePause([u8; 20]), 26 | TorrentState(TorrentState), 27 | } 28 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/app.rs: -------------------------------------------------------------------------------- 1 | use futures::{SinkExt, Stream, StreamExt}; 2 | use tokio::{ 3 | net::TcpStream, 4 | select, spawn, 5 | sync::mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender}, 6 | }; 7 | use tokio_util::codec::Framed; 8 | use tracing::debug; 9 | use vincenzo::{ 10 | config::Config, 11 | daemon_wire::{DaemonCodec, Message}, 12 | }; 13 | 14 | use crate::{ 15 | action::{self, Action}, 16 | error::Error, 17 | pages::{torrent_list::TorrentList, Page}, 18 | tui::Tui, 19 | }; 20 | 21 | pub struct App { 22 | pub is_detached: bool, 23 | pub tx: UnboundedSender, 24 | should_quit: bool, 25 | rx: Option>, 26 | page: Box, 27 | } 28 | 29 | impl App { 30 | pub fn is_detched(mut self, v: bool) -> Self { 31 | self.is_detached = v; 32 | self 33 | } 34 | 35 | pub fn new() -> Self { 36 | let (tx, rx) = unbounded_channel(); 37 | 38 | let page = Box::new(TorrentList::new(tx.clone())); 39 | 40 | App { should_quit: false, tx, rx: Some(rx), page, is_detached: false } 41 | } 42 | 43 | pub async fn run(&mut self) -> Result<(), Error> { 44 | let mut tui = Tui::new()?; 45 | tui.run()?; 46 | 47 | let tx = self.tx.clone(); 48 | let mut rx = std::mem::take(&mut self.rx).unwrap(); 49 | 50 | let daemon_addr = Config::load().unwrap().daemon_addr; 51 | let socket = TcpStream::connect(daemon_addr).await.unwrap(); 52 | 53 | // spawn event loop to listen to messages sent by the daemon 54 | let socket = Framed::new(socket, DaemonCodec); 55 | let (mut sink, stream) = socket.split(); 56 | let _tx = self.tx.clone(); 57 | 58 | let handle = spawn(async move { 59 | let _ = Self::listen_daemon(_tx, stream).await; 60 | }); 61 | 62 | loop { 63 | // block until the next event 64 | let e = tui.next().await?; 65 | let a = self.page.get_action(e); 66 | let _ = tx.send(a); 67 | 68 | while let Ok(action) = rx.try_recv() { 69 | self.page.handle_action(&action); 70 | 71 | if let Action::Render = action { 72 | let _ = tui.draw(|f| { 73 | self.page.draw(f); 74 | }); 75 | } 76 | 77 | if let Action::Quit = action { 78 | if !self.is_detached { 79 | let _ = sink.send(Message::Quit).await; 80 | handle.abort(); 81 | } 82 | tui.cancel(); 83 | self.should_quit = true; 84 | } 85 | 86 | if let Action::ChangePage(component) = action { 87 | self.handle_change_component(component)? 88 | } 89 | 90 | if let Action::NewTorrent(magnet) = action { 91 | let _ = 92 | sink.send(Message::NewTorrent(magnet.to_owned())).await; 93 | } 94 | } 95 | 96 | if self.should_quit { 97 | break; 98 | } 99 | } 100 | tui.exit()?; 101 | 102 | Ok(()) 103 | } 104 | 105 | /// Listen to the messages sent by the daemon via TCP, 106 | /// when we receive a message, we send it to ourselves 107 | /// via mpsc [`Action`]. For example, when we receive 108 | /// a TorrentState message from the daemon, we forward it to ourselves. 109 | pub async fn listen_daemon< 110 | T: Stream> + Unpin, 111 | >( 112 | tx: mpsc::UnboundedSender, 113 | mut stream: T, 114 | ) { 115 | debug!("ui listen_daemon"); 116 | println!("am i listening to daemon msgs"); 117 | loop { 118 | select! { 119 | Some(Ok(msg)) = stream.next() => { 120 | match msg { 121 | Message::TorrentState(Some(torrent_state)) => { 122 | let _ = tx.send(Action::TorrentState(torrent_state)); 123 | } 124 | Message::Quit => { 125 | println!("ui Quit - noooo"); 126 | // debug!("ui Quit"); 127 | // let _ = tx.send(Action::Quit); 128 | // break; 129 | } 130 | Message::TogglePause(torrent) => { 131 | let _ = tx.send(Action::TogglePause(torrent)); 132 | } 133 | _ => {} 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | /// Handle the logic to render another component on the screen, after 141 | /// receiving an [`Action::ChangePage`] 142 | fn handle_change_component( 143 | &mut self, 144 | page: action::Page, 145 | ) -> Result<(), Error> { 146 | self.page = match page { 147 | action::Page::TorrentList => { 148 | Box::new(TorrentList::new(self.tx.clone())) 149 | } 150 | }; 151 | Ok(()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use tokio::sync::mpsc; 3 | use vincenzo::daemon::DaemonMsg; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | #[error("Could not send message to UI")] 8 | SendErrorFr(#[from] mpsc::error::SendError), 9 | #[error("Could not send message to TCP socket: `{0}`")] 10 | SendErrorTcp(String), 11 | } 12 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod app; 3 | pub mod error; 4 | pub mod pages; 5 | pub mod tui; 6 | use ratatui::prelude::*; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct AppStyle { 10 | pub base_style: Style, 11 | pub highlight_bg: Style, 12 | pub highlight_fg: Style, 13 | pub success: Style, 14 | pub error: Style, 15 | pub warning: Style, 16 | } 17 | 18 | impl Default for AppStyle { 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl AppStyle { 25 | pub fn new() -> Self { 26 | AppStyle { 27 | base_style: Style::default().fg(Color::Gray), 28 | highlight_bg: Style::default() 29 | .bg(Color::LightBlue) 30 | .fg(Color::DarkGray), 31 | highlight_fg: Style::default().fg(Color::LightBlue), 32 | success: Style::default().fg(Color::LightGreen), 33 | error: Style::default().fg(Color::Red), 34 | warning: Style::default().fg(Color::Yellow), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/main.rs: -------------------------------------------------------------------------------- 1 | use tracing::debug; 2 | use vincenzo::error::Error; 3 | 4 | use vcz_ui::app::App; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<(), Error> { 8 | // Start and run the terminal UI 9 | let mut app = App::new(); 10 | 11 | // UI is detached from the Daemon 12 | app.is_detached = true; 13 | 14 | app.run().await.unwrap(); 15 | debug!("ui exited run"); 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | //! A page fills the entire screen and can have 0..n components. 2 | //! 3 | //! A page draws on the screen by receiving an Event and transforming it into an 4 | //! Action, and then it uses this Action on it's draw function to do whatever it 5 | //! wants. 6 | 7 | use ratatui::Frame; 8 | pub mod torrent_list; 9 | 10 | use crate::{action::Action, tui::Event}; 11 | 12 | pub trait Page { 13 | /// Draw on the screen, can also call draw on it's components. 14 | fn draw(&mut self, f: &mut Frame); 15 | 16 | /// Handle an action, for example, key presses, change to another page, etc. 17 | fn handle_action(&mut self, action: &Action); 18 | 19 | /// get an app event and transform into a page action 20 | fn get_action(&self, event: Event) -> Action; 21 | 22 | /// Focus on the next component, if available. 23 | fn focus_next(&mut self); 24 | 25 | /// Focus on the previous component, if available. 26 | fn focus_prev(&mut self); 27 | } 28 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/pages/torrent_list.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEventKind}; 2 | use hashbrown::HashMap; 3 | use ratatui::{ 4 | prelude::*, 5 | widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}, 6 | }; 7 | use tokio::sync::mpsc; 8 | use vincenzo::{ 9 | torrent::{TorrentState, TorrentStatus}, 10 | utils::to_human_readable, 11 | }; 12 | 13 | use crate::{action::Action, tui::Event, AppStyle}; 14 | 15 | use super::Page; 16 | 17 | #[derive(Clone)] 18 | pub struct TorrentList<'a> { 19 | active_torrent: Option<[u8; 20]>, 20 | cursor_position: usize, 21 | footer: List<'a>, 22 | input: String, 23 | show_popup: bool, 24 | pub focused: bool, 25 | pub state: ListState, 26 | pub style: AppStyle, 27 | pub torrent_infos: HashMap<[u8; 20], TorrentState>, 28 | pub tx: mpsc::UnboundedSender, 29 | } 30 | 31 | impl<'a> TorrentList<'a> { 32 | pub fn new(tx: mpsc::UnboundedSender) -> Self { 33 | let style = AppStyle::new(); 34 | let state = ListState::default(); 35 | let k: Line = vec![ 36 | Span::styled("k".to_string(), style.highlight_fg), 37 | " move up ".into(), 38 | Span::styled("j".to_string(), style.highlight_fg), 39 | " move down ".into(), 40 | Span::styled("t".to_string(), style.highlight_fg), 41 | " add torrent ".into(), 42 | Span::styled("p".to_string(), style.highlight_fg), 43 | " pause/resume ".into(), 44 | Span::styled("q".to_string(), style.highlight_fg), 45 | " quit".into(), 46 | ] 47 | .into(); 48 | 49 | let line = ListItem::new(k); 50 | let footer_list: Vec = vec![line]; 51 | 52 | let footer = List::new(footer_list) 53 | .block(Block::default().borders(Borders::ALL).title("Keybindings")); 54 | 55 | Self { 56 | tx, 57 | focused: true, 58 | show_popup: false, 59 | input: String::new(), 60 | style, 61 | state, 62 | active_torrent: None, 63 | torrent_infos: HashMap::new(), 64 | cursor_position: 0, 65 | footer, 66 | } 67 | } 68 | 69 | /// Go to the next torrent in the list 70 | fn next(&mut self) { 71 | if !self.torrent_infos.is_empty() { 72 | let i = self.state.selected().map_or(0, |v| { 73 | if v != self.torrent_infos.len() - 1 { 74 | v + 1 75 | } else { 76 | 0 77 | } 78 | }); 79 | self.state.select(Some(i)); 80 | } 81 | } 82 | 83 | /// Go to the previous torrent in the list 84 | fn previous(&mut self) { 85 | if !self.torrent_infos.is_empty() { 86 | let i = self.state.selected().map_or(0, |v| { 87 | if v == 0 { 88 | self.torrent_infos.len() - 1 89 | } else { 90 | v - 1 91 | } 92 | }); 93 | self.state.select(Some(i)); 94 | } 95 | } 96 | 97 | fn quit(&mut self) { 98 | self.input.clear(); 99 | if self.show_popup { 100 | self.show_popup = false; 101 | self.reset_cursor(); 102 | } else { 103 | let _ = self.tx.send(Action::Quit); 104 | } 105 | } 106 | 107 | /// Return a floating centered Rect 108 | fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect { 109 | let popup_layout = Layout::default() 110 | .direction(Direction::Vertical) 111 | .constraints( 112 | [ 113 | Constraint::Percentage((100 - percent_y) / 2), 114 | Constraint::Percentage(percent_y), 115 | Constraint::Percentage((100 - percent_y) / 2), 116 | ] 117 | .as_ref(), 118 | ) 119 | .split(r); 120 | 121 | Layout::default() 122 | .direction(Direction::Horizontal) 123 | .constraints( 124 | [ 125 | Constraint::Percentage((100 - percent_x) / 2), 126 | Constraint::Percentage(percent_x), 127 | Constraint::Percentage((100 - percent_x) / 2), 128 | ] 129 | .as_ref(), 130 | ) 131 | .split(popup_layout[1])[1] 132 | } 133 | 134 | fn move_cursor_left(&mut self) { 135 | let cursor_moved_left = self.cursor_position.saturating_sub(1); 136 | self.cursor_position = self.clamp_cursor(cursor_moved_left); 137 | } 138 | 139 | fn move_cursor_right(&mut self) { 140 | let cursor_moved_right = self.cursor_position.saturating_add(1); 141 | self.cursor_position = self.clamp_cursor(cursor_moved_right); 142 | } 143 | 144 | fn enter_char(&mut self, new_char: char) { 145 | self.input.insert(self.cursor_position, new_char); 146 | self.move_cursor_right(); 147 | } 148 | 149 | fn delete_char(&mut self) { 150 | let is_not_cursor_leftmost = self.cursor_position != 0; 151 | if is_not_cursor_leftmost { 152 | // Method "remove" is not used on the saved text for deleting the 153 | // selected char. Reason: Using remove on String works 154 | // on bytes instead of the chars. Using remove would 155 | // require special care because of char boundaries. 156 | 157 | let current_index = self.cursor_position; 158 | let from_left_to_current_index = current_index - 1; 159 | 160 | // Getting all characters before the selected character. 161 | let before_char_to_delete = 162 | self.input.chars().take(from_left_to_current_index); 163 | // Getting all characters after selected character. 164 | let after_char_to_delete = self.input.chars().skip(current_index); 165 | 166 | // Put all characters together except the selected one. 167 | // By leaving the selected one out, it is forgotten and therefore 168 | // deleted. 169 | self.input = 170 | before_char_to_delete.chain(after_char_to_delete).collect(); 171 | self.move_cursor_left(); 172 | } 173 | } 174 | 175 | fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { 176 | new_cursor_pos.clamp(0, self.input.len()) 177 | } 178 | 179 | fn reset_cursor(&mut self) { 180 | self.cursor_position = 0; 181 | } 182 | 183 | fn submit_magnet_link(&mut self) { 184 | let _ = 185 | self.tx.send(Action::NewTorrent(std::mem::take(&mut self.input))); 186 | 187 | self.quit(); 188 | } 189 | } 190 | 191 | impl<'a> Page for TorrentList<'a> { 192 | fn draw(&mut self, f: &mut ratatui::Frame) { 193 | let selected = self.state.selected(); 194 | let mut rows: Vec = Vec::new(); 195 | 196 | for (i, ctx) in self.torrent_infos.values().enumerate() { 197 | let mut download_rate = to_human_readable(ctx.download_rate as f64); 198 | download_rate.push_str("/s"); 199 | 200 | let name = Span::from(ctx.name.clone()).bold(); 201 | 202 | let status_style = match ctx.status { 203 | TorrentStatus::Seeding => self.style.success, 204 | TorrentStatus::Error => self.style.error, 205 | TorrentStatus::Paused => self.style.warning, 206 | _ => self.style.highlight_fg, 207 | }; 208 | 209 | let status_txt: &str = ctx.status.clone().into(); 210 | let mut status_txt = vec![Span::styled(status_txt, status_style)]; 211 | 212 | if ctx.status == TorrentStatus::Downloading { 213 | let download_and_rate = format!( 214 | " {} - {download_rate}", 215 | to_human_readable(ctx.downloaded as f64) 216 | ) 217 | .into(); 218 | status_txt.push(download_and_rate); 219 | } 220 | 221 | let s = ctx.stats.seeders.to_string(); 222 | let l = ctx.stats.leechers.to_string(); 223 | let sl = format!("Seeders {s} Leechers {l}").into(); 224 | 225 | let mut line_top = Line::from("-".repeat(f.area().width as usize)); 226 | let mut line_bottom = line_top.clone(); 227 | 228 | if self.state.selected() == Some(i) { 229 | line_top = line_top.patch_style(self.style.highlight_fg); 230 | line_bottom = line_bottom.patch_style(self.style.highlight_fg); 231 | } 232 | 233 | let mut items = vec![ 234 | line_top, 235 | name.into(), 236 | to_human_readable(ctx.size as f64).into(), 237 | sl, 238 | status_txt.into(), 239 | line_bottom, 240 | ]; 241 | 242 | if Some(i) == selected { 243 | self.active_torrent = Some(ctx.info_hash); 244 | } 245 | 246 | if Some(i) != selected && selected > Some(0) { 247 | items.remove(0); 248 | } 249 | 250 | rows.push(ListItem::new(items)); 251 | } 252 | 253 | let torrent_list = List::new(rows) 254 | .block(Block::default().borders(Borders::ALL).title("Torrents")); 255 | 256 | // Create two chunks, the body, and the footer 257 | let chunks = Layout::default() 258 | .direction(Direction::Vertical) 259 | .constraints([Constraint::Max(98), Constraint::Length(3)].as_ref()) 260 | .split(f.area()); 261 | 262 | if self.show_popup { 263 | let area = self.centered_rect(60, 20, f.area()); 264 | 265 | let input = Paragraph::new(self.input.as_str()) 266 | .style(self.style.highlight_fg) 267 | .block( 268 | Block::default().borders(Borders::ALL).title("Add Torrent"), 269 | ); 270 | 271 | f.render_widget(Clear, area); 272 | f.render_widget(input, area); 273 | f.set_cursor_position(Position { 274 | x: area.x + self.cursor_position as u16 + 1, 275 | y: area.y + 1, 276 | }); 277 | } else { 278 | f.render_stateful_widget(torrent_list, chunks[0], &mut self.state); 279 | f.render_widget(self.footer.clone(), chunks[1]); 280 | } 281 | } 282 | fn get_action(&self, event: crate::tui::Event) -> crate::action::Action { 283 | match event { 284 | Event::Error => Action::None, 285 | Event::Tick => Action::Tick, 286 | Event::Render => Action::Render, 287 | Event::Key(key) => Action::Key(key), 288 | Event::Quit => Action::Quit, 289 | _ => Action::None, 290 | } 291 | } 292 | fn handle_action(&mut self, action: &Action) { 293 | match action { 294 | Action::TorrentState(torrent_state) => { 295 | self.torrent_infos 296 | .insert(torrent_state.info_hash, torrent_state.clone()); 297 | } 298 | Action::Key(k) 299 | if self.show_popup && k.kind == KeyEventKind::Press => 300 | { 301 | match k.code { 302 | KeyCode::Enter => self.submit_magnet_link(), 303 | KeyCode::Char(to_insert) => { 304 | self.enter_char(to_insert); 305 | } 306 | KeyCode::Backspace => { 307 | self.delete_char(); 308 | } 309 | KeyCode::Left => { 310 | self.move_cursor_left(); 311 | } 312 | KeyCode::Right => { 313 | self.move_cursor_right(); 314 | } 315 | _ => {} 316 | } 317 | } 318 | Action::Key(k) if k.kind == KeyEventKind::Press => match k.code { 319 | KeyCode::Char('q') | KeyCode::Esc => { 320 | self.quit(); 321 | } 322 | KeyCode::Down | KeyCode::Char('j') => { 323 | self.next(); 324 | } 325 | KeyCode::Up | KeyCode::Char('k') => { 326 | self.previous(); 327 | } 328 | KeyCode::Char('t') => { 329 | self.show_popup = true; 330 | } 331 | KeyCode::Char('p') => { 332 | if let Some(active_torrent) = self.active_torrent { 333 | let _ = 334 | self.tx.send(Action::TogglePause(active_torrent)); 335 | } 336 | } 337 | _ => {} 338 | }, 339 | _ => {} 340 | } 341 | } 342 | fn focus_next(&mut self) {} 343 | fn focus_prev(&mut self) {} 344 | } 345 | -------------------------------------------------------------------------------- /crates/vcz_ui/src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use std::{ 3 | ops::{Deref, DerefMut}, 4 | time::Duration, 5 | }; 6 | 7 | use crossterm::{ 8 | cursor, 9 | event::{ 10 | DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, 11 | EnableMouseCapture, Event as CrosstermEvent, KeyEvent, KeyEventKind, 12 | MouseEvent, 13 | }, 14 | terminal::{EnterAlternateScreen, LeaveAlternateScreen}, 15 | }; 16 | use futures::{FutureExt, StreamExt}; 17 | use ratatui::backend::CrosstermBackend as Backend; 18 | use serde::{Deserialize, Serialize}; 19 | use tokio::{ 20 | sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, 21 | task::JoinHandle, 22 | }; 23 | use tokio_util::sync::CancellationToken; 24 | use tracing::error; 25 | 26 | #[derive(Clone, Debug, Serialize, Deserialize)] 27 | pub enum Event { 28 | Init, 29 | Quit, 30 | Error, 31 | Closed, 32 | Tick, 33 | Render, 34 | FocusGained, 35 | FocusLost, 36 | Paste(String), 37 | Key(KeyEvent), 38 | Mouse(MouseEvent), 39 | Resize(u16, u16), 40 | } 41 | 42 | pub struct Tui { 43 | pub terminal: ratatui::Terminal>, 44 | pub task: JoinHandle<()>, 45 | pub cancellation_token: CancellationToken, 46 | pub event_rx: UnboundedReceiver, 47 | pub event_tx: UnboundedSender, 48 | pub mouse: bool, 49 | pub paste: bool, 50 | } 51 | 52 | impl Tui { 53 | const FRAME_RATE: f64 = 60.0; 54 | const TICK_RATE: f64 = 4.0; 55 | 56 | pub fn new() -> Result { 57 | let terminal = 58 | ratatui::Terminal::new(Backend::new(std::io::stderr())).unwrap(); 59 | let (event_tx, event_rx) = mpsc::unbounded_channel(); 60 | let cancellation_token = CancellationToken::new(); 61 | let task = tokio::spawn(async {}); 62 | let mouse = false; 63 | let paste = false; 64 | 65 | Ok(Self { 66 | terminal, 67 | task, 68 | cancellation_token, 69 | event_rx, 70 | event_tx, 71 | mouse, 72 | paste, 73 | }) 74 | } 75 | 76 | pub fn mouse(mut self, mouse: bool) -> Self { 77 | self.mouse = mouse; 78 | self 79 | } 80 | 81 | pub fn paste(mut self, paste: bool) -> Self { 82 | self.paste = paste; 83 | self 84 | } 85 | 86 | fn start(&mut self) { 87 | let tick_delay = Duration::from_secs_f64(1.0 / Self::TICK_RATE); 88 | let render_delay = Duration::from_secs_f64(1.0 / Self::FRAME_RATE); 89 | 90 | self.cancel(); 91 | self.cancellation_token = CancellationToken::new(); 92 | 93 | let _cancellation_token = self.cancellation_token.clone(); 94 | let _event_tx = self.event_tx.clone(); 95 | 96 | self.task = tokio::spawn(async move { 97 | let mut reader = crossterm::event::EventStream::new(); 98 | let mut tick_interval = tokio::time::interval(tick_delay); 99 | let mut render_interval = tokio::time::interval(render_delay); 100 | 101 | _event_tx.send(Event::Init).unwrap(); 102 | 103 | loop { 104 | let tick_delay = tick_interval.tick(); 105 | let render_delay = render_interval.tick(); 106 | let crossterm_event = reader.next().fuse(); 107 | 108 | tokio::select! { 109 | _ = _cancellation_token.cancelled() => { 110 | break; 111 | } 112 | maybe_event = crossterm_event => { 113 | match maybe_event { 114 | Some(Ok(evt)) => { 115 | match evt { 116 | CrosstermEvent::Key(key) => { 117 | if key.kind == KeyEventKind::Press { 118 | _event_tx.send(Event::Key(key)).unwrap(); 119 | } 120 | }, 121 | CrosstermEvent::Mouse(mouse) => { 122 | _event_tx.send(Event::Mouse(mouse)).unwrap(); 123 | }, 124 | CrosstermEvent::Resize(x, y) => { 125 | _event_tx.send(Event::Resize(x, y)).unwrap(); 126 | }, 127 | CrosstermEvent::FocusLost => { 128 | _event_tx.send(Event::FocusLost).unwrap(); 129 | }, 130 | CrosstermEvent::FocusGained => { 131 | _event_tx.send(Event::FocusGained).unwrap(); 132 | }, 133 | CrosstermEvent::Paste(s) => { 134 | _event_tx.send(Event::Paste(s)).unwrap(); 135 | }, 136 | } 137 | } 138 | Some(Err(_)) => { 139 | _event_tx.send(Event::Error).unwrap(); 140 | } 141 | None => {}, 142 | } 143 | }, 144 | _ = tick_delay => { 145 | _event_tx.send(Event::Tick).unwrap(); 146 | }, 147 | _ = render_delay => { 148 | _event_tx.send(Event::Render).unwrap(); 149 | }, 150 | } 151 | } 152 | }); 153 | } 154 | 155 | pub fn stop(&self) -> Result<(), Error> { 156 | let mut counter = 0; 157 | self.cancel(); 158 | 159 | while !self.task.is_finished() { 160 | std::thread::sleep(Duration::from_millis(1)); 161 | counter += 1; 162 | if counter > 50 { 163 | self.task.abort(); 164 | } 165 | if counter > 100 { 166 | error!("Failed to abort task in 100 milliseconds for unknown reason"); 167 | break; 168 | } 169 | } 170 | 171 | Ok(()) 172 | } 173 | 174 | pub fn run(&mut self) -> Result<(), Error> { 175 | crossterm::terminal::enable_raw_mode().unwrap(); 176 | crossterm::execute!( 177 | std::io::stderr(), 178 | EnterAlternateScreen, 179 | cursor::Hide 180 | ) 181 | .unwrap(); 182 | if self.mouse { 183 | crossterm::execute!(std::io::stderr(), EnableMouseCapture).unwrap(); 184 | } 185 | if self.paste { 186 | crossterm::execute!(std::io::stderr(), EnableBracketedPaste) 187 | .unwrap(); 188 | } 189 | self.start(); 190 | Ok(()) 191 | } 192 | 193 | pub fn exit(&mut self) -> Result<(), Error> { 194 | self.stop()?; 195 | if crossterm::terminal::is_raw_mode_enabled().unwrap() { 196 | self.flush().unwrap(); 197 | if self.paste { 198 | crossterm::execute!(std::io::stderr(), DisableBracketedPaste) 199 | .unwrap(); 200 | } 201 | if self.mouse { 202 | crossterm::execute!(std::io::stderr(), DisableMouseCapture) 203 | .unwrap(); 204 | } 205 | crossterm::execute!( 206 | std::io::stderr(), 207 | LeaveAlternateScreen, 208 | cursor::Show 209 | ) 210 | .unwrap(); 211 | crossterm::terminal::disable_raw_mode().unwrap(); 212 | } 213 | Ok(()) 214 | } 215 | 216 | pub fn cancel(&self) { 217 | self.cancellation_token.cancel(); 218 | } 219 | 220 | pub fn suspend(&mut self) -> Result<(), Error> { 221 | self.exit()?; 222 | // #[cfg(not(windows))] 223 | // signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; 224 | Ok(()) 225 | } 226 | 227 | pub fn resume(&mut self) -> Result<(), Error> { 228 | self.run()?; 229 | Ok(()) 230 | } 231 | 232 | pub async fn next(&mut self) -> Result { 233 | Ok(self.event_rx.recv().await.unwrap()) 234 | // .ok_or(color_eyre::eyre::eyre!("Unable to get event")) 235 | } 236 | } 237 | 238 | impl Deref for Tui { 239 | type Target = ratatui::Terminal>; 240 | 241 | fn deref(&self) -> &Self::Target { 242 | &self.terminal 243 | } 244 | } 245 | 246 | impl DerefMut for Tui { 247 | fn deref_mut(&mut self) -> &mut Self::Target { 248 | &mut self.terminal 249 | } 250 | } 251 | 252 | impl Drop for Tui { 253 | fn drop(&mut self) { 254 | self.exit().unwrap(); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /crates/vincenzo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vincenzo" 3 | version = "0.0.3" 4 | description = "A BitTorrent protocol library that powers the Vincenzo client." 5 | keywords = ["distributed", "bittorrent", "torrent", "p2p", "networking"] 6 | categories = ["network-programming"] 7 | repository = "https://github.com/gabrieldemian/vincenzo" 8 | homepage = "https://vincenzo.rs" 9 | authors = ["Gabriel Lombardo "] 10 | edition = "2021" 11 | readme = "README.md" 12 | license = "MIT" 13 | exclude = ["tests/*", "*.log"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | config = { workspace = true } 19 | clap = { workspace = true } 20 | futures = { workspace = true } 21 | hashbrown = { workspace = true } 22 | rand = { workspace = true } 23 | serde = { workspace = true } 24 | thiserror = { workspace = true } 25 | tokio = { workspace = true } 26 | tokio-util = { workspace = true } 27 | bendy = { workspace = true } 28 | bytes = { workspace = true } 29 | hex = { workspace = true } 30 | magnet-url = { workspace = true } 31 | sha1_smol = { workspace = true } 32 | speedy = { workspace = true } 33 | tracing = { workspace = true } 34 | urlencoding = { workspace = true } 35 | directories = { workspace = true } 36 | toml = { workspace = true } 37 | bitvec = { workspace = true } 38 | -------------------------------------------------------------------------------- /crates/vincenzo/README.md: -------------------------------------------------------------------------------- 1 | ![image](tape.gif) 2 | 3 | # Vincenzo 4 | 5 | Vincenzo is a library for building programs powered by the BitTorrent protocol. 6 | 7 | This crate offers the building blocks of the entire protocol, so users can build anything with it: libraries, binaries, or even new UIs. 8 | 9 | ## Features 10 | - BitTorrent V1 protocol 11 | - Multi-platform 12 | - Support for magnet links 13 | - Async I/O with tokio 14 | - UDP connections with trackers 15 | - Daemon detached from UI 16 | 17 | # Example 18 | 19 | To download a torrent, we can simply run the daemon and send messages to it. 20 | 21 | ``` 22 | use vincenzo::daemon::Daemon; 23 | use vincenzo::daemon::DaemonMsg; 24 | use vincenzo::magnet::Magnet; 25 | use tokio::spawn; 26 | use tokio::sync::oneshot; 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | let download_dir = "/home/gabriel/Downloads".to_string(); 31 | 32 | let mut daemon = Daemon::new(download_dir); 33 | let tx = daemon.ctx.tx.clone(); 34 | 35 | spawn(async move { 36 | daemon.run().await.unwrap(); 37 | }); 38 | 39 | let magnet = Magnet::new("magnet:?xt=urn:btih:ab6ad7ff24b5ed3a61352a1f1a7811a8c3cc6dde&dn=archlinux-2023.09.01-x86_64.iso").unwrap(); 40 | 41 | // identifier of the torrent 42 | let info_hash = magnet.parse_xt(); 43 | 44 | tx.send(DaemonMsg::NewTorrent(magnet)).await.unwrap(); 45 | 46 | // get information about the torrent download 47 | let (otx, orx) = oneshot::channel(); 48 | 49 | tx.send(DaemonMsg::RequestTorrentState(info_hash, otx)).await.unwrap(); 50 | let torrent_state = orx.await.unwrap(); 51 | 52 | // TorrentState { 53 | // name: "torrent name", 54 | // download_rate: 999999, 55 | // ... 56 | // } 57 | } 58 | ``` 59 | 60 | ## Supported BEPs 61 | - [BEP 0003](http://www.bittorrent.org/beps/bep_0003.html) - The BitTorrent Protocol Specification 62 | - [BEP 0009](http://www.bittorrent.org/beps/bep_0009.html) - Extension for Peers to Send Metadata Files 63 | - [BEP 0010](http://www.bittorrent.org/beps/bep_0010.html) - Extension Protocol 64 | - [BEP 0015](http://www.bittorrent.org/beps/bep_0015.html) - UDP Tracker Protocol 65 | - [BEP 0023](http://www.bittorrent.org/beps/bep_0023.html) - Tracker Returns Compact Peer Lists 66 | -------------------------------------------------------------------------------- /crates/vincenzo/src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug, Default)] 4 | #[clap(name = "Vincenzo", author = "Gabriel Lombardo")] 5 | #[command(author, version, about, long_about = None)] 6 | pub struct Args { 7 | /// Download a torrent using it's magnet link, wrapped in quotes. 8 | #[clap(short, long)] 9 | pub magnet: Option, 10 | 11 | /// Print all torrent status on stdout 12 | #[clap(short, long)] 13 | pub stats: bool, 14 | 15 | /// Pause a torrent given a hash string of its id 16 | #[clap(short, long)] 17 | pub pause: Option, 18 | 19 | /// Stop all torrents and gracefully shutdown 20 | #[clap(short, long)] 21 | pub quit: bool, 22 | // /// If the program should quit after all torrents are fully 23 | // downloaded #[clap(short, long)] 24 | // pub quit_after_complete: bool, 25 | } 26 | -------------------------------------------------------------------------------- /crates/vincenzo/src/avg.rs: -------------------------------------------------------------------------------- 1 | //! Exponential moving average accumulator. 2 | use std::{convert::TryInto, time::Duration}; 3 | 4 | /// An algorithm is used that addresss the initial bias that occurs when all 5 | /// values are initialized with zero or with the first sample (which would bias 6 | /// the average toward the first value). This is achieved by initially giving 7 | /// a low gain for the average and slowly increasing it until the inverted gain 8 | /// is reached. 9 | /// 10 | /// For example, the first sample should have a gain of 1 as the average has no 11 | /// meaning. When adding the second sample, the average has some meaning, but 12 | /// since it only has one sample in it, the gain should be low. In the next 13 | /// round however, the gain may be larger. This increase is repeated until 14 | /// inverted gain is reached. This way, even early samples have a reasonable 15 | /// impact on the average, which is important in a torrent app. 16 | /// 17 | /// Ported from libtorrent: ``` 18 | #[derive(Debug)] 19 | pub struct SlidingAvg { 20 | /// The current running average, effectively the mean. 21 | /// 22 | /// This is a fixed-point value. The sample is multiplied by 64 before 23 | /// adding it. When the mean is returned, 32 is added and the sum is 24 | /// divided back by 64, to eliminate integer truncation that would 25 | /// result in a bias. Fixed-point calculation is used as the 26 | /// alternative is using floats which is slower as well as more 27 | /// cumbersome to perform conversions on (the main use case is with 28 | /// integers). 29 | mean: i64, 30 | /// The average deviation. 31 | /// 32 | /// This is a fixed-point value. The sample is multiplied by 64 before 33 | /// adding it. When the mean is returned, 32 is added and the sum is 34 | /// divided back by 64, to eliminate integer truncation that would 35 | /// result in a bias. Fixed-point calculation is used as the 36 | /// alternative is using floats which is slower as well as more 37 | /// cumbersome to perform conversions on (the main use case is with 38 | /// integers). 39 | deviation: i64, 40 | /// The number of samples received, but no more than `inverted_gain`. 41 | sample_count: usize, 42 | /// This is the threshold used for determining how many initial samples to 43 | /// give a higher gain than the current average. 44 | // TODO: turn this into a const generic parameter once that's supported 45 | inverted_gain: usize, 46 | } 47 | 48 | impl SlidingAvg { 49 | pub fn new(inverted_gain: usize) -> Self { 50 | Self { mean: 0, deviation: 0, sample_count: 0, inverted_gain } 51 | } 52 | 53 | pub fn update(&mut self, mut sample: i64) { 54 | // see comment in `Self::mean` 55 | sample *= 64; 56 | 57 | let deviation = 58 | if self.sample_count > 0 { (self.mean - sample).abs() } else { 0 }; 59 | 60 | if self.sample_count < self.inverted_gain { 61 | self.sample_count += 1; 62 | } 63 | 64 | self.mean += (sample - self.mean) / self.sample_count as i64; 65 | 66 | if self.sample_count > 1 { 67 | self.deviation += 68 | (deviation - self.deviation) / (self.sample_count - 1) as i64; 69 | } 70 | } 71 | 72 | pub fn mean(&self) -> i64 { 73 | if self.sample_count == 0 { 74 | 0 75 | } else { 76 | (self.mean + 32) / 64 77 | } 78 | } 79 | 80 | pub fn deviation(&self) -> i64 { 81 | if self.sample_count == 0 { 82 | 0 83 | } else { 84 | (self.deviation + 32) / 64 85 | } 86 | } 87 | } 88 | 89 | impl Default for SlidingAvg { 90 | /// Creates a sliding average with an inverted gain of 20. 91 | fn default() -> Self { 92 | Self::new(20) 93 | } 94 | } 95 | 96 | /// Wraps a [`SlidingAvg`] instance and converts the statistics to 97 | /// [`std::time::Duration`] units (keeping everything in the underlying layer as 98 | /// milliseconds). 99 | #[derive(Debug)] 100 | pub struct SlidingDurationAvg(SlidingAvg); 101 | 102 | impl SlidingDurationAvg { 103 | pub fn new(inverted_gain: usize) -> Self { 104 | Self(SlidingAvg::new(inverted_gain)) 105 | } 106 | 107 | pub fn update(&mut self, sample: Duration) { 108 | // TODO: is this safe? Duration::from_millis takes u64 but as_millis 109 | // returns u128 so it's not clear 110 | let ms = sample.as_millis().try_into().expect("Millisecond overflow"); 111 | self.0.update(ms); 112 | } 113 | 114 | pub fn mean(&self) -> Duration { 115 | let ms = self.0.mean() as u64; 116 | Duration::from_millis(ms) 117 | } 118 | 119 | pub fn deviation(&self) -> Duration { 120 | let ms = self.0.deviation() as u64; 121 | Duration::from_millis(ms) 122 | } 123 | } 124 | 125 | impl Default for SlidingDurationAvg { 126 | /// Creates a sliding average with an inverted gain of 20. 127 | fn default() -> Self { 128 | Self(SlidingAvg::default()) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | 136 | #[test] 137 | fn sliding_average() { 138 | let inverted_gain = 4; 139 | let mut a = SlidingAvg::new(inverted_gain); 140 | 141 | // the first sample should have a weight of 100% 142 | let sample = 10; 143 | a.update(sample); 144 | assert_eq!(a.sample_count, 1); 145 | assert_eq!(a.mean(), sample); 146 | 147 | // the second sample should have less weight 148 | let sample = 15; 149 | a.update(sample); 150 | assert_eq!(a.sample_count, 2); 151 | assert_eq!(a.mean(), 13); 152 | 153 | // the third sample even less 154 | let sample = 20; 155 | a.update(sample); 156 | assert_eq!(a.sample_count, 3); 157 | assert_eq!(a.mean(), 15); 158 | 159 | // The fourth sample reaches the inverted gain. To test that it has an 160 | // effect on the sample, always choose a sample when from which the 161 | // current mean is subtracted and divided by the sample count (which now 162 | // stops increasing) the result is an integer. For simplicity's sake 163 | // such a sample is chosen as results in the increase of the mean by 1. 164 | 165 | let sample = 19; 166 | a.update(sample); 167 | assert_eq!(a.sample_count, 4); 168 | assert_eq!(a.mean(), 16); 169 | 170 | let sample = 20; 171 | a.update(sample); 172 | assert_eq!(a.sample_count, 4); 173 | assert_eq!(a.mean(), 17); 174 | 175 | let sample = 21; 176 | a.update(sample); 177 | assert_eq!(a.sample_count, 4); 178 | assert_eq!(a.mean(), 18); 179 | 180 | // also make sure that a large sample only increases the mean by a value 181 | // proportional to its weight: that is, by (mean - sample) / 4 182 | let sample = 118; 183 | // increase should be: (118 - 18) / 4 = 25 184 | a.update(sample); 185 | assert_eq!(a.mean(), 43); 186 | } 187 | 188 | #[test] 189 | fn sliding_duration_average() { 190 | // since the implementation of the moving average is the same as for 191 | // `SlidSlidingAvg`, we only need to test that the i64 <-> duration 192 | // conversions are correct 193 | let mut a = SlidingDurationAvg::default(); 194 | 195 | // initially the mean is the same as the first sample 196 | let sample = Duration::from_secs(10); 197 | a.update(sample); 198 | assert_eq!(a.0.sample_count, 1); 199 | assert_eq!(a.mean(), sample); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /crates/vincenzo/src/bitfield.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper types around Bitvec. 2 | use bitvec::prelude::*; 3 | 4 | /// Bitfield where index = piece. 5 | pub type Bitfield = BitVec; 6 | 7 | /// Reserved bytes exchanged during handshake. 8 | pub type Reserved = BitArray<[u8; 8], Msb0>; 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use super::*; 13 | 14 | #[test] 15 | fn new_bitvec() { 16 | let a = bitvec![u8, Msb0; 0; 1]; 17 | // a.set(9, true); 18 | println!("a {a:#?}"); 19 | println!("len {:#?}", a.len()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /crates/vincenzo/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Config file 2 | use std::{net::SocketAddr, sync::LazyLock}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::error::Error; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone)] 9 | pub struct Config { 10 | pub download_dir: String, 11 | pub daemon_addr: SocketAddr, 12 | pub quit_after_complete: bool, 13 | } 14 | 15 | static CONFIG: LazyLock = LazyLock::new(|| { 16 | let home = std::env::var("HOME").expect("The $HOME env var is not set, therefore the program cant use default values, you should set them manually on the configuration file or through CLI flags. Use --help."); 17 | 18 | let download_dir = std::env::var("XDG_DOWNLOAD_DIR") 19 | .unwrap_or(format!("{home}/Downloads")); 20 | 21 | // config.toml, the .toml part is omitted. 22 | // right now this default guess only works in linux and macos. 23 | let config_file = std::env::var("XDG_CONFIG_HOME") 24 | .map(|v| format!("{v}/vincenzo/config")) 25 | .unwrap_or(format!("{home}/.config/vincenzo/config")); 26 | 27 | config::Config::builder() 28 | .add_source(config::File::with_name(&config_file).required(false)) 29 | .add_source(config::Environment::default()) 30 | .set_default("download_dir", download_dir) 31 | .unwrap() 32 | .set_default("daemon_addr", "127.0.0.1:3030") 33 | .unwrap() 34 | .set_default("quit_after_complete", false) 35 | .unwrap() 36 | .build() 37 | .unwrap() 38 | }); 39 | 40 | impl Config { 41 | /// Try to load the configuration. Environmental variables have priviledge 42 | /// over values from the configuration file. If both are not set, it will 43 | /// try to guess the default values using $HOME. 44 | pub fn load() -> Result { 45 | CONFIG 46 | .clone() 47 | .try_deserialize::() 48 | .map_err(|_| Error::ConfigDeserializeError) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | pub mod tests { 54 | use super::*; 55 | 56 | #[test] 57 | fn override_config() { 58 | std::env::set_var("DOWNLOAD_DIR", "/new/download"); 59 | 60 | let parsed = Config::load().unwrap(); 61 | 62 | assert_eq!(parsed.download_dir, "/new/download".to_owned()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/vincenzo/src/counter.rs: -------------------------------------------------------------------------------- 1 | // there are some APIs that are not being used at the moment but are going to be 2 | // used when new features are added 3 | use std::ops::AddAssign; 4 | 5 | /// Counts statistics about the communication channels used in torrents. 6 | #[derive(Clone, Copy, Debug, Default)] 7 | pub struct ThruputCounters { 8 | /// Counts protocol chatter, which are the exchanged non-payload related 9 | /// messages (such as 'unchoke', 'have', 'request', etc). 10 | pub protocol: ChannelCounter, 11 | /// Counts the exchanged block bytes. This only include the block's data, 12 | /// minus the header, which counts towards the protocol chatter. 13 | pub payload: ChannelCounter, 14 | /// Counts the (downloaded) payload bytes that were wasted (i.e. duplicate 15 | /// blocks that had to be discarded). 16 | pub waste: Counter, 17 | } 18 | 19 | impl ThruputCounters { 20 | /// Resets the per-round accummulators of the counters. 21 | /// 22 | /// This should be called once a second to provide accurate per second 23 | /// thruput rates. 24 | pub fn reset(&mut self) { 25 | self.protocol.reset(); 26 | self.payload.reset(); 27 | self.waste.reset(); 28 | } 29 | } 30 | 31 | impl AddAssign<&ThruputCounters> for ThruputCounters { 32 | fn add_assign(&mut self, rhs: &ThruputCounters) { 33 | self.protocol += &rhs.protocol; 34 | self.payload += &rhs.payload; 35 | self.waste += rhs.waste.round(); 36 | } 37 | } 38 | 39 | /// Counts statistics about a communication channel (such as protocol chatter or 40 | /// payload transfer), both the ingress and engress sides. 41 | #[derive(Clone, Copy, Debug, Default)] 42 | pub struct ChannelCounter { 43 | pub down: Counter, 44 | pub up: Counter, 45 | } 46 | 47 | impl ChannelCounter { 48 | /// Resets the per-round accummulators of the counters. 49 | /// 50 | /// This should be called once a second to provide accurate per second 51 | /// thruput rates. 52 | pub fn reset(&mut self) { 53 | self.down.reset(); 54 | self.up.reset(); 55 | } 56 | } 57 | 58 | impl AddAssign<&ChannelCounter> for ChannelCounter { 59 | fn add_assign(&mut self, rhs: &ChannelCounter) { 60 | self.down += rhs.down.round(); 61 | self.up += rhs.up.round(); 62 | } 63 | } 64 | 65 | /// Used for counting the running average of throughput rates. 66 | /// 67 | /// This counts the total bytes transferred, as well as the current round's 68 | /// tally. Then, at the end of each round, the caller is responsible for calling 69 | /// [`Counter::reset`] which updates the running average and clears the 70 | /// per round counter. 71 | /// 72 | /// The tallied throughput rate is the 5 second weighed running average. It is 73 | /// produced as follows: 74 | /// 75 | /// avg = (avg * 4/5) + (this_round / 5) 76 | /// 77 | /// This way a temporary deviation in one round does not punish the overall 78 | /// download rate disproportionately. 79 | #[derive(Clone, Copy, Debug, Default)] 80 | pub struct Counter { 81 | total: u64, 82 | round: u64, 83 | avg: f64, 84 | peak: f64, 85 | } 86 | 87 | impl Counter { 88 | // TODO: turn this into a const generic parameter once that's supported 89 | const WEIGHT: u64 = 5; 90 | 91 | /// Records some bytes that were transferred. 92 | pub fn add(&mut self, bytes: u64) { 93 | self.total += bytes; 94 | self.round += bytes; 95 | } 96 | 97 | /// Finishes counting this round and updates the 5 second moving average. 98 | /// 99 | /// # Important 100 | /// 101 | /// This assumes that this function is called once a second. 102 | pub fn reset(&mut self) { 103 | // https://github.com/arvidn/libtorrent/blob/master/src/stat.cpp 104 | self.avg = (self.avg * (Self::WEIGHT - 1) as f64 / Self::WEIGHT as f64) 105 | + (self.round as f64 / Self::WEIGHT as f64); 106 | self.round = 0; 107 | 108 | if self.avg > self.peak { 109 | self.peak = self.avg; 110 | } 111 | } 112 | 113 | /// Returns the 5 second moving average, rounded to the nearest integer. 114 | pub fn avg(&self) -> u64 { 115 | self.avg.round() as u64 116 | } 117 | 118 | /// Returns the average recorded so far, rounded to the nearest integer. 119 | pub fn peak(&self) -> u64 { 120 | self.peak.round() as u64 121 | } 122 | 123 | /// Returns the total number recorded. 124 | pub fn total(&self) -> u64 { 125 | self.total 126 | } 127 | 128 | /// Returns the number recorded in the current round. 129 | pub fn round(&self) -> u64 { 130 | self.round 131 | } 132 | } 133 | 134 | impl AddAssign for Counter { 135 | fn add_assign(&mut self, rhs: u64) { 136 | self.add(rhs); 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | 144 | #[test] 145 | fn test_counter() { 146 | let mut c = Counter::default(); 147 | 148 | assert_eq!(c.avg(), 0); 149 | assert_eq!(c.peak(), 0); 150 | assert_eq!(c.round(), 0); 151 | assert_eq!(c.total(), 0); 152 | 153 | c += 5; 154 | assert_eq!(c.round(), 5); 155 | assert_eq!(c.total(), 5); 156 | 157 | c.reset(); 158 | // 4 * 0 / 5 + 5 / 5 = 1 159 | assert_eq!(c.avg(), 1); 160 | assert_eq!(c.peak(), 1); 161 | assert_eq!(c.round(), 0); 162 | assert_eq!(c.total(), 5); 163 | 164 | c += 10; 165 | assert_eq!(c.round(), 10); 166 | assert_eq!(c.total(), 15); 167 | 168 | c.reset(); 169 | // 4 * 1 / 5 + 10 / 5 = 0.8 + 2 = 2.8 ~ 3 170 | assert_eq!(c.avg(), 3); 171 | assert_eq!(c.peak(), 3); 172 | assert_eq!(c.round(), 0); 173 | assert_eq!(c.total(), 15); 174 | 175 | c += 30; 176 | assert_eq!(c.round(), 30); 177 | assert_eq!(c.total(), 45); 178 | 179 | c.reset(); 180 | // 4 * 2.8 / 5 + 30 / 5 = 2.24 + 6 = 8.24 ~ 8 181 | assert_eq!(c.avg(), 8); 182 | assert_eq!(c.peak(), 8); 183 | assert_eq!(c.round(), 0); 184 | assert_eq!(c.total(), 45); 185 | 186 | c += 1; 187 | assert_eq!(c.round(), 1); 188 | assert_eq!(c.total(), 46); 189 | 190 | c.reset(); 191 | // 4 * 8.24 / 5 + 1 / 5 = 6.592 + 0.2 = 6.792 ~ 7 192 | assert_eq!(c.avg(), 7); 193 | assert_eq!(c.peak(), 8); 194 | assert_eq!(c.round(), 0); 195 | assert_eq!(c.total(), 46); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /crates/vincenzo/src/daemon.rs: -------------------------------------------------------------------------------- 1 | //! A daemon that runs on the background and handles everything 2 | //! that is not the UI. 3 | use futures::{SinkExt, StreamExt}; 4 | use hashbrown::HashMap; 5 | use std::{ 6 | net::{IpAddr, Ipv4Addr, SocketAddr}, 7 | sync::Arc, 8 | time::Duration, 9 | }; 10 | use tokio_util::codec::Framed; 11 | use tracing::{error, info, trace, warn}; 12 | 13 | use tokio::{ 14 | net::{TcpListener, TcpStream}, 15 | select, spawn, 16 | sync::{mpsc, oneshot, RwLock}, 17 | time::interval, 18 | }; 19 | 20 | use crate::{ 21 | config::Config, 22 | daemon_wire::{DaemonCodec, Message}, 23 | disk::{Disk, DiskMsg}, 24 | error::Error, 25 | magnet::Magnet, 26 | torrent::{Torrent, TorrentMsg, TorrentState, TorrentStatus}, 27 | utils::to_human_readable, 28 | }; 29 | 30 | /// The daemon is the highest-level entity in the library. 31 | /// It owns [`Disk`] and [`Torrent`]s, which owns Peers. 32 | /// 33 | /// The communication with the daemon happens via TCP with messages 34 | /// documented at [`DaemonCodec`]. 35 | /// 36 | /// The daemon is decoupled from the UI and can even run on different machines, 37 | /// and so, they need a way to communicate. We use TCP, so we can benefit 38 | /// from the Framed utilities that tokio provides, making it easy 39 | /// to create a protocol for the Daemon. HTTP wastes more bandwith 40 | /// and would reduce consistency since the BitTorrent protocol nowadays rarely 41 | /// uses HTTP. 42 | pub struct Daemon { 43 | // pub config: DaemonConfig, 44 | pub disk_tx: Option>, 45 | pub ctx: Arc, 46 | /// key: info_hash 47 | pub torrent_txs: HashMap<[u8; 20], mpsc::Sender>, 48 | rx: mpsc::Receiver, 49 | } 50 | 51 | /// Context of the [`Daemon`] that may be shared between other types. 52 | pub struct DaemonCtx { 53 | pub tx: mpsc::Sender, 54 | /// key: info_hash 55 | /// States of all Torrents, updated each second by the Torrent struct. 56 | pub torrent_states: RwLock>, 57 | } 58 | 59 | /// Messages used by the [`Daemon`] for internal communication. 60 | /// All of these local messages have an equivalent remote message 61 | /// on [`DaemonMsg`]. 62 | #[derive(Debug)] 63 | pub enum DaemonMsg { 64 | /// Tell Daemon to add a new torrent and it will immediately 65 | /// announce to a tracker, connect to the peers, and start the download. 66 | NewTorrent(Magnet), 67 | /// Message that the Daemon will send to all connectors when the state 68 | /// of a torrent updates (every 1 second). 69 | TorrentState(TorrentState), 70 | /// Ask the Daemon to send a [`TorrentState`] of the torrent with the given 71 | /// hash_info. 72 | RequestTorrentState([u8; 20], oneshot::Sender>), 73 | /// Pause/Resume a torrent. 74 | TogglePause([u8; 20]), 75 | /// Gracefully shutdown the Daemon 76 | Quit, 77 | /// Print the status of all Torrents to stdout 78 | PrintTorrentStatus, 79 | } 80 | 81 | impl Daemon { 82 | pub const DEFAULT_LISTENER: SocketAddr = 83 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 3030); 84 | 85 | /// Initialize the Daemon struct with the default [`DaemonConfig`]. 86 | pub fn new() -> Self { 87 | let (tx, rx) = mpsc::channel::(300); 88 | 89 | Self { 90 | rx, 91 | disk_tx: None, 92 | torrent_txs: HashMap::new(), 93 | ctx: Arc::new(DaemonCtx { 94 | tx, 95 | torrent_states: RwLock::new(HashMap::new()), 96 | }), 97 | } 98 | } 99 | 100 | /// This function will listen to 3 different event loops: 101 | /// - The daemon internal messages via MPSC [`DaemonMsg`] (external) 102 | /// - The daemon TCP framed messages [`DaemonCodec`] (internal) 103 | /// - The Disk event loop [`Disk`] 104 | /// 105 | /// # Important 106 | /// 107 | /// Both internal and external messages share the same API. 108 | /// When the daemon receives a TCP message, it forwards to the 109 | /// mpsc event loop. 110 | /// 111 | /// This is useful to keep consistency, because the same command 112 | /// that can be fired remotely (via TCP), 113 | /// can also be fired internaly (via CLI flags). 114 | pub async fn run(&mut self) -> Result<(), Error> { 115 | let config = Config::load()?; 116 | let socket = TcpListener::bind(config.daemon_addr).await.unwrap(); 117 | 118 | let (disk_tx, disk_rx) = mpsc::channel::(300); 119 | self.disk_tx = Some(disk_tx); 120 | 121 | let mut disk = Disk::new(disk_rx, config.download_dir); 122 | 123 | spawn(async move { 124 | let _ = disk.run().await; 125 | }); 126 | 127 | let ctx = self.ctx.clone(); 128 | 129 | info!("Daemon listening on: {}", config.daemon_addr); 130 | 131 | // Listen to remote TCP messages 132 | let handle = spawn(async move { 133 | loop { 134 | match socket.accept().await { 135 | Ok((socket, addr)) => { 136 | info!("Connected with remote: {addr}"); 137 | 138 | let ctx = ctx.clone(); 139 | 140 | spawn(async move { 141 | let socket = Framed::new(socket, DaemonCodec); 142 | let _ = Self::listen_remote_msgs(socket, ctx).await; 143 | }); 144 | } 145 | Err(e) => { 146 | error!("Could not connect with remote: {e:#?}"); 147 | } 148 | } 149 | } 150 | }); 151 | 152 | let ctx = self.ctx.clone(); 153 | 154 | // Listen to internal mpsc messages 155 | loop { 156 | select! { 157 | Some(msg) = self.rx.recv() => { 158 | match msg { 159 | DaemonMsg::TorrentState(torrent_state) => { 160 | let mut torrent_states = self.ctx.torrent_states.write().await; 161 | 162 | torrent_states.insert(torrent_state.info_hash, torrent_state.clone()); 163 | 164 | if config.quit_after_complete && torrent_states.values().all(|v| v.status == TorrentStatus::Seeding) { 165 | let _ = ctx.tx.send(DaemonMsg::Quit).await; 166 | } 167 | 168 | drop(torrent_states); 169 | } 170 | DaemonMsg::NewTorrent(magnet) => { 171 | let _ = self.new_torrent(magnet).await; 172 | } 173 | DaemonMsg::TogglePause(info_hash) => { 174 | let _ = self.toggle_pause(info_hash).await; 175 | } 176 | DaemonMsg::RequestTorrentState(info_hash, recipient) => { 177 | let torrent_states = self.ctx.torrent_states.read().await; 178 | let torrent_state = torrent_states.get(&info_hash); 179 | let _ = recipient.send(torrent_state.cloned()); 180 | } 181 | DaemonMsg::PrintTorrentStatus => { 182 | let torrent_states = self.ctx.torrent_states.read().await; 183 | 184 | println!("Showing stats of {} torrents.", torrent_states.len()); 185 | 186 | for state in torrent_states.values() { 187 | let status_line: String = match state.status { 188 | TorrentStatus::Downloading => { 189 | format!( 190 | "{} - {}", 191 | to_human_readable(state.downloaded as f64), 192 | to_human_readable(state.download_rate as f64), 193 | ) 194 | } 195 | _ => state.status.clone().into() 196 | }; 197 | 198 | println!( 199 | "\n{}\n{}\nSeeders {} Leechers {}\n{status_line}", 200 | state.name, 201 | to_human_readable(state.size as f64), 202 | state.stats.seeders, 203 | state.stats.leechers, 204 | ); 205 | } 206 | } 207 | DaemonMsg::Quit => { 208 | let _ = self.quit().await; 209 | handle.abort(); 210 | break; 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | Ok(()) 218 | } 219 | 220 | /// Listen to messages sent remotely via TCP, 221 | /// A UI can be a standalone binary that is executing on another machine, 222 | /// and wants to control the daemon using the [`DaemonCodec`] protocol. 223 | async fn listen_remote_msgs( 224 | socket: Framed, 225 | ctx: Arc, 226 | ) -> Result<(), Error> { 227 | trace!("daemon listen_msgs"); 228 | 229 | let mut draw_interval = interval(Duration::from_secs(1)); 230 | let (mut sink, mut stream) = socket.split(); 231 | 232 | loop { 233 | select! { 234 | // listen to messages sent remotely via TCP, and pass them 235 | // to our rx. We do this so we can use the exact same messages 236 | // when sent remotely via TCP (i.e UI on remote server), 237 | // or locally on the same binary (i.e CLI). 238 | Some(Ok(msg)) = stream.next() => { 239 | match msg { 240 | Message::NewTorrent(magnet_link) => { 241 | trace!("daemon received NewTorrent {magnet_link}"); 242 | let magnet = Magnet::new(&magnet_link); 243 | if let Ok(magnet) = magnet { 244 | let _ = ctx.tx.send(DaemonMsg::NewTorrent(magnet)).await; 245 | } 246 | } 247 | Message::RequestTorrentState(info_hash) => { 248 | trace!("daemon RequestTorrentState {info_hash:?}"); 249 | let (tx, rx) = oneshot::channel(); 250 | let _ = ctx.tx.send(DaemonMsg::RequestTorrentState(info_hash, tx)).await; 251 | let r = rx.await?; 252 | 253 | let _ = sink.send(Message::TorrentState(r)).await; 254 | } 255 | Message::TogglePause(id) => { 256 | trace!("daemon received TogglePause {id:?}"); 257 | let _ = ctx.tx.send(DaemonMsg::TogglePause(id)).await; 258 | } 259 | Message::Quit => { 260 | info!("Daemon is quitting"); 261 | let _ = ctx.tx.send(DaemonMsg::Quit).await; 262 | } 263 | Message::PrintTorrentStatus => { 264 | trace!("daemon received PrintTorrentStatus"); 265 | let _ = ctx.tx.send(DaemonMsg::PrintTorrentStatus).await; 266 | } 267 | _ => {} 268 | } 269 | } 270 | // listen to messages sent locally, from the daemon binary. 271 | // a Torrent that is owned by the Daemon, may send messages to this channel 272 | _ = draw_interval.tick() => { 273 | let _ = Self::draw(&mut sink, ctx.clone()).await; 274 | } 275 | } 276 | } 277 | } 278 | 279 | /// Pause/resume the torrent, making the download an upload stale. 280 | pub async fn toggle_pause(&self, info_hash: [u8; 20]) -> Result<(), Error> { 281 | let tx = self 282 | .torrent_txs 283 | .get(&info_hash) 284 | .ok_or(Error::TorrentDoesNotExist)?; 285 | 286 | tx.send(TorrentMsg::TogglePause).await?; 287 | 288 | Ok(()) 289 | } 290 | 291 | /// Sends a Draw message to the [`UI`] with the updated state of a torrent. 292 | async fn draw(sink: &mut T, ctx: Arc) -> Result<(), Error> 293 | where 294 | T: SinkExt + Sized + std::marker::Unpin + Send, 295 | { 296 | let torrent_states = ctx.torrent_states.read().await; 297 | 298 | for state in torrent_states.values().cloned() { 299 | // debug!("{state:#?}"); 300 | sink.send(Message::TorrentState(Some(state))) 301 | .await 302 | .map_err(|_| Error::SendErrorTcp)?; 303 | } 304 | 305 | drop(torrent_states); 306 | Ok(()) 307 | } 308 | 309 | /// Create a new [`Torrent`] given a magnet link URL 310 | /// and run the torrent's event loop. 311 | /// 312 | /// # Errors 313 | /// 314 | /// This fn may return an [`Err`] if the magnet link is invalid 315 | /// 316 | /// # Panic 317 | /// 318 | /// This fn will panic if it is being called BEFORE run 319 | pub async fn new_torrent(&mut self, magnet: Magnet) -> Result<(), Error> { 320 | trace!("magnet: {}", *magnet); 321 | let info_hash = magnet.parse_xt(); 322 | 323 | let mut torrent_states = self.ctx.torrent_states.write().await; 324 | 325 | if torrent_states.get(&info_hash).is_some() { 326 | warn!("This torrent is already present on the Daemon"); 327 | return Err(Error::NoDuplicateTorrent); 328 | } 329 | 330 | let torrent_state = TorrentState { 331 | name: magnet.parse_dn(), 332 | info_hash, 333 | ..Default::default() 334 | }; 335 | 336 | torrent_states.insert(info_hash, torrent_state); 337 | drop(torrent_states); 338 | 339 | // disk_tx is not None at this point, this is safe 340 | // (if calling after run) 341 | let disk_tx = self.disk_tx.clone().unwrap(); 342 | let mut torrent = Torrent::new(disk_tx, self.ctx.tx.clone(), magnet); 343 | 344 | self.torrent_txs.insert(info_hash, torrent.ctx.tx.clone()); 345 | info!("Downloading torrent: {}", torrent.name); 346 | 347 | spawn(async move { 348 | torrent.start_and_run(None).await?; 349 | Ok::<(), Error>(()) 350 | }); 351 | 352 | Ok(()) 353 | } 354 | 355 | async fn quit(&mut self) -> Result<(), Error> { 356 | // tell all torrents that we are gracefully shutting down, 357 | // each torrent will kill their peers tasks, and their tracker task 358 | for (_, tx) in std::mem::take(&mut self.torrent_txs) { 359 | spawn(async move { 360 | let _ = tx.send(TorrentMsg::Quit).await; 361 | }); 362 | } 363 | let _ = self 364 | .disk_tx 365 | .as_ref() 366 | .map(|tx| async { tx.send(DiskMsg::Quit).await }); 367 | Ok(()) 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /crates/vincenzo/src/daemon_wire/mod.rs: -------------------------------------------------------------------------------- 1 | //! Framed messages sent to/from Daemon 2 | use bytes::{Buf, BufMut, BytesMut}; 3 | use speedy::{BigEndian, Readable, Writable}; 4 | use std::io::Cursor; 5 | use tokio::io; 6 | use tokio_util::codec::{Decoder, Encoder}; 7 | 8 | use crate::torrent::TorrentState; 9 | 10 | /// Messages of [`DaemonCodec`], check the struct documentation 11 | /// to read how to send messages. 12 | /// 13 | /// Most messages can be sent to the Daemon in 2 ways: 14 | /// - Internally: within it's same process, via CLI flags for example. the 15 | /// message will be sent using mpsc. 16 | /// - Externally: via TCP. When the message arrives, it will be sent 17 | /// to the internal event handler in mpsc. They both use the same API. 18 | #[derive(Debug, Clone, PartialEq)] 19 | pub enum Message { 20 | /// Daemon will send other Quit messages to all Torrents. 21 | /// and Disk. It will close all event loops spawned through `run`. 22 | /// 23 | /// Quit does not have a message_id, only the u32 len. 24 | Quit, 25 | /// Add a new torrent given a magnet link. 26 | /// 27 | /// 28 | NewTorrent(String), 29 | /// Every second, the Daemon will send information about all torrents 30 | /// to all listeners 31 | /// 32 | /// 33 | TorrentState(Option), 34 | /// Pause/Resume the torrent with the given info_hash. 35 | TogglePause([u8; 20]), 36 | /// Ask the Daemon to send a [`TorrentState`] of the torrent with the given 37 | /// hash_info. 38 | RequestTorrentState([u8; 20]), 39 | /// Print the status of all Torrents to stdout 40 | PrintTorrentStatus, 41 | } 42 | 43 | #[repr(u8)] 44 | #[derive(Copy, Clone, Debug, PartialEq)] 45 | pub enum MessageId { 46 | NewTorrent = 1, 47 | TorrentState = 2, 48 | GetTorrentState = 3, 49 | TogglePause = 4, 50 | PrintTorrentStatus = 5, 51 | } 52 | 53 | impl TryFrom for MessageId { 54 | type Error = io::Error; 55 | 56 | fn try_from(k: u8) -> Result { 57 | use MessageId::*; 58 | match k { 59 | k if k == NewTorrent as u8 => Ok(NewTorrent), 60 | k if k == TorrentState as u8 => Ok(TorrentState), 61 | k if k == GetTorrentState as u8 => Ok(GetTorrentState), 62 | k if k == PrintTorrentStatus as u8 => Ok(PrintTorrentStatus), 63 | k if k == TogglePause as u8 => Ok(TogglePause), 64 | _ => Err(io::Error::new( 65 | io::ErrorKind::InvalidInput, 66 | "Unknown message id", 67 | )), 68 | } 69 | } 70 | } 71 | 72 | /// The daemon messages follow the same logic as the peer messages: 73 | /// The first `u32` is the len of the entire payload that comes after itself. 74 | /// Followed by an `u8` which is the message_id. The rest of the bytes 75 | /// depends on the message type. 76 | /// 77 | /// In other words: 78 | /// len,msg_id,payload 79 | /// u32 u8 x (in bits) 80 | /// 81 | /// # Example 82 | /// 83 | /// You are sending a magnet of 18 bytes: "magnet:blabla" 84 | /// 85 | /// ``` 86 | /// use bytes::{Buf, BufMut, BytesMut}; 87 | /// use vincenzo::daemon_wire::MessageId; 88 | /// 89 | /// let mut buf = BytesMut::new(); 90 | /// let magnet = "magnet:blabla".to_owned(); 91 | /// 92 | /// // len is: 1 byte of the message_id + the payload len 93 | /// let msg_len = 1 + magnet.len() as u32; 94 | /// 95 | /// // len> 96 | /// buf.put_u32(msg_len); 97 | /// 98 | /// // msg_id message_id is 1 99 | /// buf.put_u8(MessageId::NewTorrent as u8); 100 | /// 101 | /// // payload 102 | /// buf.extend_from_slice(magnet.as_bytes()); 103 | /// 104 | /// // result 105 | /// // len msg_id payload 106 | /// // 19 1 "magnet:blabla" 107 | /// // u32 u8 (dynamic size) 108 | /// ``` 109 | #[derive(Debug)] 110 | pub struct DaemonCodec; 111 | 112 | // From message to bytes 113 | impl Encoder for DaemonCodec { 114 | type Error = io::Error; 115 | 116 | fn encode( 117 | &mut self, 118 | item: Message, 119 | buf: &mut BytesMut, 120 | ) -> Result<(), Self::Error> { 121 | match item { 122 | Message::NewTorrent(magnet) => { 123 | let msg_len = 1 + magnet.len() as u32; 124 | 125 | buf.put_u32(msg_len); 126 | buf.put_u8(MessageId::NewTorrent as u8); 127 | buf.extend_from_slice(magnet.as_bytes()); 128 | } 129 | Message::TorrentState(torrent_info) => { 130 | let info_bytes = match torrent_info { 131 | Some(v) => v.write_to_vec_with_ctx(BigEndian {})?, 132 | None => vec![], 133 | }; 134 | let msg_len = 1 + info_bytes.len() as u32; 135 | 136 | buf.put_u32(msg_len); 137 | buf.put_u8(MessageId::TorrentState as u8); 138 | buf.extend_from_slice(&info_bytes); 139 | } 140 | Message::RequestTorrentState(info_hash) => { 141 | let msg_len = 1 + info_hash.len() as u32; 142 | 143 | buf.put_u32(msg_len); 144 | buf.put_u8(MessageId::GetTorrentState as u8); 145 | buf.extend_from_slice(&info_hash); 146 | } 147 | Message::TogglePause(info_hash) => { 148 | let msg_len = 1 + info_hash.len() as u32; 149 | 150 | buf.put_u32(msg_len); 151 | buf.put_u8(MessageId::TogglePause as u8); 152 | buf.extend_from_slice(&info_hash); 153 | } 154 | Message::PrintTorrentStatus => { 155 | let msg_len = 1; 156 | 157 | buf.put_u32(msg_len); 158 | buf.put_u8(MessageId::PrintTorrentStatus as u8); 159 | } 160 | Message::Quit => { 161 | buf.put_u32(0); 162 | } 163 | } 164 | Ok(()) 165 | } 166 | } 167 | 168 | // From bytes to message 169 | impl Decoder for DaemonCodec { 170 | type Item = Message; 171 | type Error = io::Error; 172 | 173 | fn decode( 174 | &mut self, 175 | buf: &mut BytesMut, 176 | ) -> Result, Self::Error> { 177 | // the message length header must be present at the minimum, otherwise 178 | // we can't determine the message type 179 | if buf.remaining() < 4 { 180 | return Ok(None); 181 | } 182 | 183 | // `get_*` integer extractors consume the message bytes by advancing 184 | // buf's internal cursor. However, we don't want to do this as at this 185 | // point we aren't sure we have the full message in the buffer, and thus 186 | // we just want to peek at this value. 187 | let mut tmp_buf = Cursor::new(&buf); 188 | let msg_len = tmp_buf.get_u32() as usize; 189 | 190 | tmp_buf.set_position(0); 191 | 192 | if buf.remaining() >= 4 + msg_len { 193 | // we have the full message in the buffer so advance the buffer 194 | // cursor past the message length header 195 | buf.advance(4); 196 | 197 | // Only a Quit doesnt have ID 198 | if msg_len == 0 { 199 | return Ok(Some(Message::Quit)); 200 | } 201 | } else { 202 | tracing::trace!( 203 | "Read buffer is {} bytes long but message is {} bytes long", 204 | buf.remaining(), 205 | msg_len 206 | ); 207 | return Ok(None); 208 | } 209 | 210 | let msg_id = MessageId::try_from(buf.get_u8())?; 211 | 212 | // here, buf is already advanced past the len and msg_id, 213 | // so all calls to `remaining` and `get_*` will start from the payload. 214 | let msg = match msg_id { 215 | MessageId::NewTorrent => { 216 | let mut payload = vec![0u8; buf.remaining()]; 217 | buf.copy_to_slice(&mut payload); 218 | 219 | Message::NewTorrent(String::from_utf8(payload).unwrap()) 220 | } 221 | MessageId::TorrentState => { 222 | let mut info: Option = None; 223 | 224 | if buf.has_remaining() { 225 | let mut payload = vec![0u8; buf.remaining()]; 226 | buf.copy_to_slice(&mut payload); 227 | info = TorrentState::read_from_buffer_with_ctx( 228 | BigEndian {}, 229 | &payload, 230 | ) 231 | .ok(); 232 | } 233 | Message::TorrentState(info) 234 | } 235 | MessageId::TogglePause => { 236 | let mut payload = [0u8; 20_usize]; 237 | buf.copy_to_slice(&mut payload); 238 | 239 | Message::TogglePause(payload) 240 | } 241 | MessageId::PrintTorrentStatus => Message::PrintTorrentStatus, 242 | MessageId::GetTorrentState => { 243 | let mut payload = [0u8; 20_usize]; 244 | buf.copy_to_slice(&mut payload); 245 | 246 | Message::RequestTorrentState(payload) 247 | } 248 | }; 249 | 250 | Ok(Some(msg)) 251 | } 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use crate::torrent::TorrentStatus; 257 | 258 | use super::*; 259 | 260 | #[test] 261 | fn new_torrent() { 262 | let mut buf = BytesMut::new(); 263 | let msg = Message::NewTorrent("magnet:blabla".to_owned()); 264 | DaemonCodec.encode(msg, &mut buf).unwrap(); 265 | 266 | println!("encoded {buf:?}"); 267 | 268 | let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap(); 269 | 270 | println!("decoded {msg:?}"); 271 | 272 | match msg { 273 | Message::NewTorrent(magnet) => { 274 | assert_eq!(magnet, "magnet:blabla".to_owned()); 275 | } 276 | _ => panic!(), 277 | } 278 | } 279 | 280 | #[test] 281 | fn torrent_state() { 282 | let info = TorrentState { 283 | name: "Eesti".to_owned(), 284 | stats: crate::torrent::Stats { 285 | interval: 5, 286 | leechers: 9, 287 | seeders: 1, 288 | }, 289 | status: TorrentStatus::Downloading, 290 | downloaded: 999, 291 | download_rate: 111, 292 | uploaded: 44, 293 | size: 9, 294 | info_hash: [0u8; 20], 295 | }; 296 | 297 | let a = info.write_to_vec_with_ctx(BigEndian {}).unwrap(); 298 | println!("encoding a {a:?}"); 299 | 300 | let mut buf = BytesMut::new(); 301 | let msg = Message::TorrentState(Some(info.clone())); 302 | DaemonCodec.encode(msg, &mut buf).unwrap(); 303 | 304 | let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap(); 305 | 306 | match msg { 307 | Message::TorrentState(deserialized) => { 308 | assert_eq!(deserialized, Some(info)); 309 | } 310 | _ => panic!(), 311 | } 312 | 313 | // should send None to inexistent torrent 314 | let mut buf = BytesMut::new(); 315 | let msg = Message::TorrentState(None); 316 | DaemonCodec.encode(msg, &mut buf).unwrap(); 317 | 318 | let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap(); 319 | match msg { 320 | Message::TorrentState(r) => { 321 | assert_eq!(r, None); 322 | } 323 | _ => panic!(), 324 | } 325 | } 326 | 327 | #[test] 328 | fn request_torrent_state() { 329 | let mut buf = BytesMut::new(); 330 | let msg = Message::RequestTorrentState([1u8; 20]); 331 | DaemonCodec.encode(msg, &mut buf).unwrap(); 332 | 333 | println!("encoded {buf:?}"); 334 | 335 | let msg = DaemonCodec.decode(&mut buf).unwrap().unwrap(); 336 | 337 | println!("decoded {msg:?}"); 338 | 339 | match msg { 340 | Message::RequestTorrentState(info_hash) => { 341 | assert_eq!(info_hash, [1u8; 20]); 342 | } 343 | _ => panic!(), 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /crates/vincenzo/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use thiserror::Error; 4 | use tokio::sync::{mpsc, oneshot}; 5 | 6 | use crate::{ 7 | disk::DiskMsg, peer::PeerMsg, torrent::TorrentMsg, tracker::TrackerMsg, 8 | }; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum Error { 12 | #[error("Failed to send a connect request to the tracker")] 13 | ConnectSendFailed, 14 | #[error("Failed to decode or encode the bencode buffer")] 15 | BencodeError, 16 | #[error("IO error")] 17 | PeerSocketAddrs(#[from] io::Error), 18 | #[error("Peer resolved to no unusable addresses")] 19 | PeerSocketAddr, 20 | #[error("Tracker resolved to no unusable addresses")] 21 | TrackerNoHosts, 22 | #[error("Peer resolved to no unusable addresses")] 23 | TrackerSocketAddr, 24 | #[error("The response received from the connect handshake was wrong")] 25 | TrackerResponse, 26 | #[error( 27 | "Tracker event only goes from 0..=2 and a different value was used" 28 | )] 29 | TrackerEvent, 30 | #[error("Tried to call announce without calling connect first")] 31 | TrackerResponseLength, 32 | #[error("The response length received from the tracker was less then 20 bytes, when it should be larger")] 33 | TrackerNoConnectionId, 34 | #[error("The peer list returned by the announce request is not valid")] 35 | TrackerCompactPeerList, 36 | #[error("Could not connect to the UDP socket of the tracker")] 37 | TrackerSocketConnect, 38 | #[error("Error when serializing/deserializing")] 39 | SpeedyError(#[from] speedy::Error), 40 | #[error("Error when reading magnet link")] 41 | MagnetLinkInvalid, 42 | #[error("The response received from the peer is wrong")] 43 | MessageResponse, 44 | #[error("The request took to long to arrive")] 45 | RequestTimeout, 46 | #[error("The message took to long to arrive")] 47 | MessageTimeout, 48 | #[error("The handshake received is not valid")] 49 | HandshakeInvalid, 50 | #[error( 51 | "Could not open the file `{0}`. Please make sure the program has permission to access it" 52 | )] 53 | FileOpenError(String), 54 | #[error( 55 | "Could not open the folder `{0}`. Please make sure the program has permission to open it and that the folder exist" 56 | )] 57 | FolderOpenError(String), 58 | #[error("The `{0}` folder was not found, please edit the config file manually at `{1}")] 59 | FolderNotFound(String, String), 60 | #[error("This torrent is already downloaded fully")] 61 | TorrentComplete, 62 | #[error("Could not find torrent for the given info_hash")] 63 | TorrentDoesNotExist, 64 | #[error("The piece downloaded does not have a valid hash")] 65 | PieceInvalid, 66 | #[error("The peer ID does not exist on this torrent")] 67 | PeerIdInvalid, 68 | #[error("Disk does not have the provided info_hash")] 69 | InfoHashInvalid, 70 | #[error("The peer took to long to respond")] 71 | Timeout, 72 | #[error("Your magnet does not have a tracker. Currently, this client does not support DHT, you need to use a magnet that has a tracker.")] 73 | MagnetNoTracker, 74 | #[error( 75 | "Your magnet does not have an info_hash, are you sure you copied the entire magnet link?" 76 | )] 77 | MagnetNoInfoHash, 78 | #[error("Could not send message to Disk")] 79 | SendErrorDisk(#[from] mpsc::error::SendError), 80 | #[error("Could not receive message from oneshot")] 81 | ReceiveErrorOneshot(#[from] oneshot::error::RecvError), 82 | #[error("Could not send message to Peer")] 83 | SendErrorPeer(#[from] mpsc::error::SendError), 84 | #[error("Could not send message to Tracker")] 85 | SendErrorTracker(#[from] mpsc::error::SendError), 86 | #[error("Could not send message to UI")] 87 | SendErrorTorrent(#[from] mpsc::error::SendError), 88 | #[error("The given PATH is invalid")] 89 | PathInvalid, 90 | #[error("Could not send message to TCP socket")] 91 | SendErrorTcp, 92 | #[error("Tried to load $HOME but could not find it. Please make sure you have a $HOME env and that this program has the permission to create dirs.")] 93 | HomeInvalid, 94 | #[error("Error while trying to read the configuration file, please make sure it has the correct format")] 95 | ConfigDeserializeError, 96 | #[error("You cannot add a duplicate torrent, only 1 is allowed")] 97 | NoDuplicateTorrent, 98 | #[error("No peers in the torrent")] 99 | NoPeers, 100 | } 101 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/core/handshake_codec.rs: -------------------------------------------------------------------------------- 1 | //! Codec for encoding and decoding handshakes. 2 | //! 3 | //! This has to be a separate codec as the handshake has a different 4 | //! structure than the rest of the messages. Moreover, handshakes may only 5 | //! be sent once at the beginning of a connection, preceding all other 6 | //! messages. Thus, after receiving and sending a handshake the codec 7 | //! should be switched to [`PeerCodec`], but care should be taken not to 8 | //! discard the underlying receive and send buffers. 9 | 10 | use std::{io, io::Cursor}; 11 | use tracing::warn; 12 | 13 | use bytes::{BufMut, BytesMut}; 14 | use speedy::{BigEndian, Readable, Writable}; 15 | use tokio_util::codec::{Decoder, Encoder}; 16 | 17 | use crate::extensions::core::PSTR; 18 | 19 | use bytes::Buf; 20 | 21 | use crate::error::Error; 22 | 23 | #[derive(Debug)] 24 | pub struct HandshakeCodec; 25 | 26 | impl Encoder for HandshakeCodec { 27 | type Error = io::Error; 28 | 29 | fn encode( 30 | &mut self, 31 | handshake: Handshake, 32 | buf: &mut BytesMut, 33 | ) -> io::Result<()> { 34 | let Handshake { pstr_len, pstr, reserved, info_hash, peer_id } = 35 | handshake; 36 | 37 | // protocol length prefix 38 | debug_assert_eq!(pstr_len, 19); 39 | 40 | buf.put_u8(pstr.len() as u8); 41 | 42 | // we should only be sending the bittorrent protocol string 43 | debug_assert_eq!(pstr, PSTR); 44 | 45 | // payload 46 | buf.extend_from_slice(&pstr); 47 | buf.extend_from_slice(&reserved); 48 | buf.extend_from_slice(&info_hash); 49 | buf.extend_from_slice(&peer_id); 50 | 51 | Ok(()) 52 | } 53 | } 54 | 55 | impl Decoder for HandshakeCodec { 56 | type Item = Handshake; 57 | type Error = io::Error; 58 | 59 | fn decode(&mut self, buf: &mut BytesMut) -> io::Result> { 60 | if buf.is_empty() { 61 | return Ok(None); 62 | } 63 | 64 | // `get_*` integer extractors consume the message bytes by advancing 65 | // buf's internal cursor. However, we don't want to do this as at this 66 | // point we aren't sure we have the full message in the buffer, and thus 67 | // we just want to peek at this value. 68 | let mut tmp_buf = Cursor::new(&buf); 69 | let prot_len = tmp_buf.get_u8() as usize; 70 | if prot_len != PSTR.len() { 71 | return Err(io::Error::new( 72 | io::ErrorKind::InvalidInput, 73 | "Handshake must have the string \"BitTorrent protocol\"", 74 | )); 75 | } 76 | 77 | // check that we got the full payload in the buffer (NOTE: we need to 78 | // add the message length prefix's byte count to msg_len since the 79 | // buffer cursor was not advanced and thus we need to consider the 80 | // prefix too) 81 | let payload_len = prot_len + 8 + 20 + 20; 82 | if buf.remaining() > payload_len { 83 | // we have the full message in the buffer so advance the buffer 84 | // cursor past the message length header 85 | buf.advance(1); 86 | } else { 87 | return Ok(None); 88 | } 89 | 90 | // protocol string 91 | let mut pstr = [0; 19]; 92 | buf.copy_to_slice(&mut pstr); 93 | // reserved field 94 | let mut reserved = [0; 8]; 95 | buf.copy_to_slice(&mut reserved); 96 | // info hash 97 | let mut info_hash = [0; 20]; 98 | buf.copy_to_slice(&mut info_hash); 99 | // peer id 100 | let mut peer_id = [0; 20]; 101 | buf.copy_to_slice(&mut peer_id); 102 | 103 | Ok(Some(Handshake { 104 | pstr, 105 | pstr_len: pstr.len() as u8, 106 | reserved, 107 | info_hash, 108 | peer_id, 109 | })) 110 | } 111 | } 112 | 113 | /// pstrlen = 19 114 | /// pstr = "BitTorrent protocol" 115 | /// This is the very first message exchanged. If the peer's protocol string 116 | /// (`BitTorrent protocol`) or the info hash differs from ours, the connection 117 | /// is severed. The reserved field is 8 zero bytes, but will later be used to 118 | /// set which extensions the peer supports. The peer id is usually the client 119 | /// name and version. 120 | #[derive(Clone, Debug, Writable, Readable)] 121 | pub struct Handshake { 122 | pub pstr_len: u8, 123 | pub pstr: [u8; 19], 124 | pub reserved: [u8; 8], 125 | pub info_hash: [u8; 20], 126 | pub peer_id: [u8; 20], 127 | } 128 | 129 | impl Handshake { 130 | pub fn new(info_hash: [u8; 20], peer_id: [u8; 20]) -> Self { 131 | let mut reserved = [0u8; 8]; 132 | 133 | // we support the `extension protocol` 134 | // set the bit 44 to the left 135 | reserved[5] |= 0x10; 136 | 137 | Self { 138 | pstr_len: u8::to_be(19), 139 | pstr: PSTR, 140 | reserved, 141 | info_hash, 142 | peer_id, 143 | } 144 | } 145 | pub fn serialize(&self) -> Result<[u8; 68], Error> { 146 | let mut buf: [u8; 68] = [0u8; 68]; 147 | let temp = self 148 | .write_to_vec_with_ctx(BigEndian {}) 149 | .map_err(Error::SpeedyError)?; 150 | 151 | buf.copy_from_slice(&temp[..]); 152 | 153 | Ok(buf) 154 | } 155 | pub fn deserialize(buf: &[u8]) -> Result { 156 | Self::read_from_buffer_with_ctx(BigEndian {}, buf) 157 | .map_err(Error::SpeedyError) 158 | } 159 | pub fn validate(&self, target: &Self) -> bool { 160 | if target.peer_id.len() != 20 { 161 | warn!("! invalid peer_id from receiving handshake"); 162 | return false; 163 | } 164 | if self.info_hash != target.info_hash { 165 | warn!("! info_hash from receiving handshake does not match ours"); 166 | return false; 167 | } 168 | if target.pstr_len != 19 { 169 | warn!("! handshake with wrong pstr_len, dropping connection"); 170 | return false; 171 | } 172 | if target.pstr != PSTR { 173 | warn!("! handshake with wrong pstr, dropping connection"); 174 | return false; 175 | } 176 | true 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | pub mod tests { 182 | use super::*; 183 | 184 | #[test] 185 | fn handshake() { 186 | let info_hash = [5u8; 20]; 187 | let peer_id = [7u8; 20]; 188 | let our_handshake = Handshake::new(info_hash, peer_id); 189 | 190 | assert_eq!(our_handshake.pstr_len, 19); 191 | assert_eq!(our_handshake.pstr, PSTR); 192 | assert_eq!(our_handshake.peer_id, peer_id); 193 | assert_eq!(our_handshake.info_hash, info_hash); 194 | 195 | let our_handshake = 196 | Handshake::new(info_hash, peer_id).serialize().unwrap(); 197 | assert_eq!( 198 | our_handshake, 199 | [ 200 | 19, 66, 105, 116, 84, 111, 114, 114, 101, 110, 116, 32, 112, 201 | 114, 111, 116, 111, 99, 111, 108, 0, 0, 0, 0, 0, 16, 0, 0, 5, 202 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 7, 203 | 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 204 | ] 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/core/message.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! as_expr { 3 | ($e:expr) => { 4 | $e 5 | }; 6 | } 7 | #[macro_export] 8 | macro_rules! as_item { 9 | ($i:item) => { 10 | $i 11 | }; 12 | } 13 | #[macro_export] 14 | macro_rules! as_pat { 15 | ($p:pat) => { 16 | $p 17 | }; 18 | } 19 | #[macro_export] 20 | macro_rules! as_stmt { 21 | ($s:stmt) => { 22 | $s 23 | }; 24 | } 25 | #[macro_export] 26 | macro_rules! as_ty { 27 | ($t:ty) => { 28 | $t 29 | }; 30 | } 31 | #[macro_export] 32 | macro_rules! as_ident { 33 | ($t:ident) => { 34 | $t 35 | }; 36 | } 37 | 38 | macro_rules! count { 39 | () => (0usize); 40 | ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*)); 41 | } 42 | 43 | use std::future::Future; 44 | 45 | use futures::{Sink, SinkExt}; 46 | 47 | use crate::{ 48 | extensions::{ 49 | core::{Core, CoreCodec}, 50 | extended::codec::ExtendedCodec, 51 | metadata::codec::MetadataCodec, 52 | }, 53 | peer::Peer, 54 | }; 55 | 56 | pub struct Message2 57 | where 58 | T: ExtensionTrait, 59 | { 60 | bytes: Vec, 61 | extension: T, 62 | } 63 | 64 | /// From a list of types that implement 65 | /// [`crate::extensions::extended::ExtensionTrait`], generate a [`Message`] 66 | /// enum with all of their messages. Aditionally, a struct `MessageCodec` that 67 | /// implements Encoder and Decoder for it's branches. 68 | /// 69 | /// example: 70 | /// 71 | /// ```ignore 72 | /// declare_message!(CoreCodec, ExtendedCodec, MetadataCodec); 73 | /// 74 | /// pub enum Message { 75 | /// CoreCodec(Core), 76 | /// ExtendedCodec(Extended), 77 | /// MetadataCodec(Metadata), 78 | /// } 79 | /// ``` 80 | #[macro_export] 81 | macro_rules! declare_message { 82 | ( $($codec: tt),* ) => { 83 | use tokio_util::codec::{Decoder, Encoder}; 84 | use crate::extensions::extended::{ExtensionTrait, MessageTrait}; 85 | use crate::error::Error; 86 | 87 | #[derive(Debug)] 88 | pub struct MessageCodec; 89 | 90 | /// A network message exchanged between Peers, each branch represents a possible 91 | /// protocol that the message may be. 92 | #[derive(Debug, Clone, PartialEq)] 93 | pub enum Message { 94 | $( 95 | $codec(<$codec as ExtensionTrait>::Msg), 96 | )* 97 | } 98 | 99 | $( 100 | impl From<<$codec as ExtensionTrait>::Msg> for Message { 101 | fn from(value: <$codec as ExtensionTrait>::Msg) -> Self { 102 | Message::$codec(value) 103 | } 104 | } 105 | )* 106 | 107 | impl Encoder for MessageCodec { 108 | type Error = Error; 109 | 110 | fn encode( 111 | &mut self, 112 | item: Message, 113 | dst: &mut bytes::BytesMut, 114 | ) -> Result<(), Self::Error> { 115 | match item { 116 | $( 117 | Message::$codec(v) => { 118 | $codec.encode(v, dst)?; 119 | }, 120 | )* 121 | }; 122 | Ok(()) 123 | } 124 | } 125 | 126 | impl Decoder for MessageCodec { 127 | type Error = Error; 128 | type Item = Message; 129 | 130 | fn decode( 131 | &mut self, 132 | src: &mut bytes::BytesMut, 133 | ) -> Result, Self::Error> { 134 | let core = CoreCodec.decode(src)?; 135 | 136 | // todo: change this error 137 | let core = core.ok_or(crate::error::Error::PeerIdInvalid)?; 138 | 139 | match core { 140 | $( 141 | // find if there is an extension that supports the given message extension 142 | // ID (src) by comparing their ids. 143 | Core::Extended(id, _payload) if id == <$codec as ExtensionTrait>::ID => { 144 | let v = $codec.codec().decode(src)? 145 | .ok_or(crate::error::Error::PeerIdInvalid)?; 146 | return Ok(Some(Message::$codec(v))); 147 | }, 148 | )* 149 | // if not, its a Core message 150 | _ => Ok(Some(Message::CoreCodec(core))) 151 | } 152 | } 153 | } 154 | 155 | $( 156 | impl MessageTrait for <$codec as ExtensionTrait>::Msg { 157 | fn codec(&self) -> impl Encoder + Decoder + ExtensionTrait::Msg> 158 | { 159 | $codec 160 | } 161 | } 162 | )* 163 | 164 | impl Message { 165 | // pub async fn handle_msg( 166 | // &self, 167 | // peer: &mut Peer, 168 | // sink: &mut T, 169 | // ) -> Result<(), Error> 170 | pub async fn handle_msg( 171 | &self, 172 | peer: &mut Peer, 173 | sink: &mut C, 174 | ) -> Result<(), Error> 175 | // where 176 | // T: SinkExt 177 | // + Sized 178 | // + std::marker::Unpin 179 | // + Send 180 | // + Sync 181 | // + Sink 182 | where 183 | M: From + Into, 184 | C: SinkExt + Sized + std::marker::Unpin + Send + Sync + Sink, 185 | { 186 | match self { 187 | $( 188 | Message::$codec(msg) => { 189 | let codec = msg.codec(); 190 | if codec.is_supported(&peer.extension) { 191 | codec.handle_msg(&msg, peer, sink).await?; 192 | } 193 | } 194 | )* 195 | } 196 | Ok(()) 197 | } 198 | } 199 | // pub async fn tick(&mut self, sink: &mut C) -> Result<(), Error> 200 | // where 201 | // M: From + Into, 202 | // C: SinkExt + Sized + std::marker::Unpin, 203 | 204 | // pub struct Extensions; 205 | // 206 | // impl Extensions { 207 | // pub fn get() -> [$($codec,)*] { 208 | // // todo!() 209 | // // [count!($($codec)*); $($codec)*] 210 | // [$($codec,)*] 211 | // } 212 | // } 213 | }; 214 | } 215 | 216 | declare_message!(CoreCodec, ExtendedCodec, MetadataCodec); 217 | // impl Message { 218 | // pub async fn handle_msg( 219 | // &self, 220 | // msg: &M, 221 | // peer: &mut Peer, 222 | // sink: &mut T, 223 | // ) -> Result<(), Error> 224 | // where 225 | // M: TryInto, 226 | // T: SinkExt 227 | // + Sized 228 | // + std::marker::Unpin 229 | // + Send 230 | // + Sync 231 | // + Sink 232 | // { 233 | // // match self { 234 | // // $( 235 | // // Message::$codec(m) => { 236 | // // let c = m.codec(); 237 | // // // if c.is_supported(&self.extension) { 238 | // // c.handle_msg(&m, peer, sink).await?; 239 | // // // } 240 | // // } 241 | // // )* 242 | // // } 243 | // Ok(()) 244 | // } 245 | // } 246 | 247 | pub struct Extensions; 248 | 249 | impl Extensions { 250 | pub fn get() -> (CoreCodec, ExtendedCodec) { 251 | (CoreCodec, ExtendedCodec) 252 | } 253 | } 254 | 255 | #[cfg(test)] 256 | mod tests { 257 | use crate::extensions::core::CoreId; 258 | 259 | use super::*; 260 | 261 | #[test] 262 | fn declare_message_works() { 263 | // use super::{Core, CoreCodec, MetadataCodec}; 264 | 265 | // declare_message!(CoreCodec, MetadataCodec); 266 | 267 | let c = Core::Interested; 268 | let id = CoreId::Interested as u8; 269 | let m = Message::CoreCodec(c); 270 | 271 | let mut buff = bytes::BytesMut::new(); 272 | MessageCodec.encode(m, &mut buff).unwrap(); 273 | 274 | println!("{:?}", buff.to_vec()); 275 | 276 | assert_eq!(buff.to_vec(), vec![0, 0, 0, 1, id]); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! Documentation of the "TCP Wire" protocol between Peers in the network. 2 | //! Peers will follow this protocol to exchange information about torrents. 3 | 4 | mod codec; 5 | mod handshake_codec; 6 | mod message; 7 | 8 | // re-exports 9 | pub use codec::*; 10 | pub use handshake_codec::*; 11 | pub use message::*; 12 | 13 | use bytes::{BufMut, BytesMut}; 14 | use tokio::io; 15 | 16 | /// The default block_len that most clients support, some clients drop 17 | /// the connection on blocks larger than this value. 18 | /// 19 | /// Tha last block of a piece might be smaller. 20 | pub const BLOCK_LEN: u32 = 16384; 21 | 22 | /// Protocol String (PSTR) 23 | /// Bytes of the string "BitTorrent protocol". Used during handshake. 24 | pub const PSTR: [u8; 19] = [ 25 | 66, 105, 116, 84, 111, 114, 114, 101, 110, 116, 32, 112, 114, 111, 116, 26 | 111, 99, 111, 108, 27 | ]; 28 | 29 | /// A Block is a subset of a Piece, 30 | /// pieces are subsets of the entire Torrent data. 31 | /// 32 | /// Blocks may overlap pieces, for example, part of a block may start at piece 33 | /// 0, but end at piece 1. 34 | /// 35 | /// When peers send data (seed) to us, they send us Blocks. 36 | /// This happens on the "Piece" message of the peer wire protocol. 37 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 38 | pub struct Block { 39 | /// The index of the piece this block belongs to. 40 | pub index: usize, 41 | /// The zero-based byte offset into the piece. 42 | pub begin: u32, 43 | /// The block's data. 16 KiB most of the times, 44 | /// but the last block of a piece *might* be smaller. 45 | pub block: Vec, 46 | } 47 | 48 | impl Block { 49 | /// Encodes the block info in the network binary protocol's format into the 50 | /// given buffer. 51 | pub fn encode(&self, buf: &mut BytesMut) -> io::Result<()> { 52 | let piece_index = self 53 | .index 54 | .try_into() 55 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; 56 | buf.put_u32(piece_index); 57 | buf.put_u32(self.begin); 58 | buf.extend_from_slice(&self.block); 59 | Ok(()) 60 | } 61 | 62 | /// Validate the [`Block`]. Like most clients, we only support 63 | /// data <= 16kiB. 64 | pub fn is_valid(&self) -> bool { 65 | self.block.len() <= BLOCK_LEN as usize && self.begin <= BLOCK_LEN 66 | } 67 | } 68 | 69 | /// The representation of a [`Block`]. 70 | /// 71 | /// When we ask a peer to give us a [`Block`], we send this struct, 72 | /// using the "Request" message of the tcp wire protocol. 73 | /// 74 | /// This is almost identical to the [`Block`] struct, 75 | /// the only difference is that instead of having a `block`, 76 | /// we have a `len` representing the len of the block. 77 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 78 | pub struct BlockInfo { 79 | /// The index of the piece of which this is a block. 80 | pub index: u32, 81 | /// The zero-based byte offset into the piece. 82 | pub begin: u32, 83 | /// The block's length in bytes. <= 16 KiB 84 | pub len: u32, 85 | } 86 | 87 | impl Default for BlockInfo { 88 | fn default() -> Self { 89 | Self { index: 0, begin: 0, len: BLOCK_LEN } 90 | } 91 | } 92 | 93 | impl From for BlockInfo { 94 | fn from(val: Block) -> Self { 95 | BlockInfo { 96 | index: val.index as u32, 97 | begin: val.begin, 98 | len: val.block.len() as u32, 99 | } 100 | } 101 | } 102 | 103 | impl BlockInfo { 104 | pub fn new() -> Self { 105 | Self::default() 106 | } 107 | pub fn index(mut self, index: u32) -> Self { 108 | self.index = index; 109 | self 110 | } 111 | pub fn begin(mut self, begin: u32) -> Self { 112 | self.begin = begin; 113 | self 114 | } 115 | pub fn len(mut self, len: u32) -> Self { 116 | self.len = len; 117 | self 118 | } 119 | /// Encodes the block info in the network binary protocol's format into the 120 | /// given buffer. 121 | pub fn encode(&self, buf: &mut BytesMut) -> io::Result<()> { 122 | buf.put_u32(self.index); 123 | buf.put_u32(self.begin); 124 | buf.put_u32(self.len); 125 | Ok(()) 126 | } 127 | /// Validate the [`BlockInfo`]. Like most clients, we only support 128 | /// data <= 16kiB. 129 | pub fn is_valid(&self) -> bool { 130 | self.len <= BLOCK_LEN && self.begin <= BLOCK_LEN && self.len > 0 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/extended/codec.rs: -------------------------------------------------------------------------------- 1 | //! Types for the Extended protocol codec. 2 | 3 | use crate::{ 4 | error::Error, 5 | extensions::core::{CoreCodec, Message}, 6 | peer::{Direction, Peer}, 7 | }; 8 | use std::{fmt::Debug, ops::Deref}; 9 | 10 | use bendy::{decoding::FromBencode, encoding::ToBencode}; 11 | use futures::{Sink, SinkExt}; 12 | use tokio_util::codec::{Decoder, Encoder}; 13 | use tracing::debug; 14 | 15 | use crate::extensions::core::Core; 16 | 17 | use super::{Extension, ExtensionTrait}; 18 | 19 | /// Extended handshake from the Extended protocol, other extended messages have 20 | /// their own enum type. 21 | #[derive(Debug, Clone, PartialEq)] 22 | pub struct Extended(Extension); 23 | 24 | impl From for Extended { 25 | fn from(value: Extension) -> Self { 26 | Self(value) 27 | } 28 | } 29 | 30 | impl Deref for Extended { 31 | type Target = Extension; 32 | fn deref(&self) -> &Self::Target { 33 | &self.0 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct ExtendedCodec; 39 | 40 | impl TryInto for Extended { 41 | type Error = Error; 42 | 43 | /// Try to convert an [`Extended`] message to a [`Core::Extended`] message. 44 | fn try_into(self) -> Result { 45 | let bytes = self.to_bencode().map_err(|_| Error::BencodeError)?; 46 | Ok(Core::Extended(0, bytes)) 47 | } 48 | } 49 | 50 | impl TryInto for Core { 51 | type Error = Error; 52 | 53 | /// Try to convert a [`Core::Extended`] to [`Extended`] message. 54 | fn try_into(self) -> Result { 55 | let ext_id = ::ID; 56 | 57 | if let Core::Extended(id, payload) = self { 58 | if id != ext_id { 59 | // todo: change this error 60 | return Err(crate::error::Error::PeerIdInvalid); 61 | } 62 | let ext = Extension::from_bencode(&payload) 63 | .map_err(|_| Error::BencodeError)?; 64 | return Ok(Extended(ext)); 65 | } 66 | // todo: change this error 67 | Err(crate::error::Error::PeerIdInvalid) 68 | } 69 | } 70 | 71 | impl Encoder for ExtendedCodec { 72 | type Error = crate::error::Error; 73 | 74 | fn encode( 75 | &mut self, 76 | item: Extended, 77 | dst: &mut bytes::BytesMut, 78 | ) -> Result<(), Self::Error> { 79 | // Core::Extended 80 | let core: Core = item.try_into()?; 81 | CoreCodec.encode(core, dst).map_err(|e| e.into()) 82 | } 83 | } 84 | 85 | impl Decoder for ExtendedCodec { 86 | type Error = crate::error::Error; 87 | type Item = Extended; 88 | 89 | fn decode( 90 | &mut self, 91 | src: &mut bytes::BytesMut, 92 | ) -> Result, Self::Error> { 93 | let core: Option = CoreCodec.decode(src)?; 94 | // todo: change this error 95 | let core = core.ok_or(Error::PeerIdInvalid)?; 96 | let extended: Extended = core.try_into()?; 97 | Ok(Some(extended)) 98 | } 99 | } 100 | 101 | impl ExtensionTrait for ExtendedCodec { 102 | type Codec = ExtendedCodec; 103 | type Msg = Extended; 104 | 105 | const ID: u8 = 0; 106 | 107 | async fn handle_msg< 108 | T: SinkExt 109 | + Sized 110 | + std::marker::Unpin 111 | + Sink, 112 | >( 113 | &self, 114 | msg: &Self::Msg, 115 | peer: &mut Peer, 116 | sink: &mut T, 117 | ) -> Result<(), Error> { 118 | debug!( 119 | "{} extended handshake from {}", 120 | peer.ctx.local_addr, peer.ctx.remote_addr 121 | ); 122 | peer.extension = msg.0.clone(); 123 | 124 | if peer.ctx.direction == Direction::Outbound { 125 | let metadata_size = peer.extension.metadata_size.unwrap(); 126 | 127 | // create our Extension dict, that the local client supports. 128 | let ext = Extension::supported(Some(metadata_size)) 129 | .to_bencode() 130 | .map_err(|_| Error::BencodeError)?; 131 | 132 | // and send to the remote peer 133 | let core = Core::Extended(Self::ID, ext); 134 | 135 | sink.send(core.into()).await?; 136 | 137 | peer.try_request_info(sink).await?; 138 | } 139 | Ok(()) 140 | } 141 | 142 | fn is_supported(&self, extension: &Extension) -> bool { 143 | extension.v.is_some() 144 | } 145 | 146 | fn codec(&self) -> Self::Codec { 147 | ExtendedCodec 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/extended/trait.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::Error, extensions::core::Message}; 2 | use std::future::Future; 3 | 4 | use futures::{Sink, SinkExt}; 5 | use tokio_util::codec::{Decoder, Encoder}; 6 | 7 | use crate::{extensions::core::Core, peer::Peer}; 8 | 9 | use super::Extension; 10 | 11 | pub trait MessageTrait: TryInto { 12 | /// Return the Codec for Self, which is a Message type. 13 | fn codec( 14 | &self, 15 | ) -> impl Encoder + Decoder + ExtensionTrait; 16 | } 17 | 18 | /// All extensions from the extended protocol (Bep 0010) must implement this 19 | /// trait. 20 | pub trait ExtensionTrait: Clone { 21 | /// The Message of the extension must know how to convert itself to a 22 | /// [`Core::Extended`] 23 | type Msg: TryInto; 24 | 25 | /// Codec for [`Self::Msg`] 26 | type Codec: Encoder + Decoder + Clone; 27 | 28 | /// The ID of this extension. 29 | const ID: u8; 30 | 31 | fn codec(&self) -> Self::Codec; 32 | 33 | /// Given an Extension dict return a boolean if the extension "Self" is 34 | /// supported or not. 35 | fn is_supported(&self, extension: &Extension) -> bool; 36 | 37 | fn handle_msg( 38 | &self, 39 | msg: &Self::Msg, 40 | peer: &mut Peer, 41 | sink: &mut T, 42 | ) -> impl Future> + Send + Sync 43 | where 44 | T: SinkExt 45 | + Sized 46 | + std::marker::Unpin 47 | + Send 48 | + Sync 49 | + Sink; 50 | } 51 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/metadata/codec.rs: -------------------------------------------------------------------------------- 1 | //! Types for the metadata protocol codec. 2 | 3 | use crate::{ 4 | error::Error, extensions::core::Message, peer::Peer, torrent::TorrentMsg, 5 | }; 6 | use bendy::encoding::ToBencode; 7 | use futures::SinkExt; 8 | use tokio::sync::oneshot; 9 | use tokio_util::codec::{Decoder, Encoder}; 10 | use tracing::{debug, info}; 11 | 12 | use crate::extensions::{ 13 | core::{Core, CoreCodec, CoreId}, 14 | extended::ExtensionTrait, 15 | }; 16 | 17 | use super::{Metadata as MetadataDict, MetadataMsgType}; 18 | 19 | /// Messages of the extended metadata protocol, used to exchange pieces of the 20 | /// `Info` of a metadata file. 21 | #[derive(Debug, Clone, PartialEq)] 22 | pub enum Metadata { 23 | /// id: 0 24 | /// Request(piece) 25 | Request(u32), 26 | /// id: 1, also named "Data" 27 | Response { 28 | metadata: crate::extensions::metadata::Metadata, 29 | payload: Vec, 30 | }, 31 | /// id: 2 32 | /// Reject(piece) 33 | Reject(u32), 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct MetadataCodec; 38 | 39 | impl Encoder for MetadataCodec { 40 | type Error = Error; 41 | 42 | fn encode( 43 | &mut self, 44 | item: Metadata, 45 | dst: &mut bytes::BytesMut, 46 | ) -> Result<(), Self::Error> { 47 | let item: Core = item.try_into()?; 48 | CoreCodec.encode(item, dst).map_err(|e| e.into()) 49 | } 50 | } 51 | 52 | impl Decoder for MetadataCodec { 53 | type Error = Error; 54 | type Item = Metadata; 55 | 56 | fn decode( 57 | &mut self, 58 | src: &mut bytes::BytesMut, 59 | ) -> Result, Self::Error> { 60 | let core = CoreCodec.decode(src)?; 61 | // todo: change this error 62 | let core = core.ok_or(crate::error::Error::PeerIdInvalid)?; 63 | let metadata: Metadata = core.try_into()?; 64 | Ok(Some(metadata)) 65 | } 66 | } 67 | 68 | impl TryInto for Core { 69 | type Error = crate::error::Error; 70 | 71 | /// Parse [`Core::Extended`] into a [`Metadata`] message. 72 | fn try_into(self) -> Result { 73 | if let Core::Extended(id, payload) = self { 74 | let ext_id = ::ID; 75 | 76 | if id != ext_id { 77 | // todo: change this error 78 | return Err(Error::PeerIdInvalid); 79 | } 80 | 81 | let (metadata, payload) = MetadataDict::extract(payload)?; 82 | 83 | return Ok(match metadata.msg_type { 84 | MetadataMsgType::Request => Metadata::Request(metadata.piece), 85 | MetadataMsgType::Reject => Metadata::Reject(metadata.piece), 86 | MetadataMsgType::Response => { 87 | Metadata::Response { metadata, payload } 88 | } 89 | }); 90 | } 91 | // todo: change this error 92 | Err(Error::PeerIdInvalid) 93 | } 94 | } 95 | 96 | impl TryInto for Metadata { 97 | type Error = Error; 98 | 99 | /// Try to convert a Metadata message to a [`Core::Extended`] message. 100 | fn try_into(self) -> Result { 101 | let id = CoreId::Extended as u8; 102 | 103 | Ok(match self { 104 | Self::Reject(piece) => Core::Extended( 105 | id, 106 | MetadataDict::reject(piece).to_bencode().unwrap(), 107 | ), 108 | Self::Request(piece) => Core::Extended( 109 | id, 110 | MetadataDict::request(piece).to_bencode().unwrap(), 111 | ), 112 | Self::Response { metadata, payload } => { 113 | let mut buff = metadata.to_bencode().unwrap(); 114 | buff.copy_from_slice(&payload); 115 | Core::Extended(id, buff) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | impl ExtensionTrait for MetadataCodec { 122 | type Codec = MetadataCodec; 123 | type Msg = Metadata; 124 | 125 | const ID: u8 = 3; 126 | 127 | async fn handle_msg + Sized + std::marker::Unpin>( 128 | &self, 129 | msg: &Self::Msg, 130 | peer: &mut Peer, 131 | sink: &mut T, 132 | ) -> Result<(), Error> { 133 | match &msg { 134 | Metadata::Response { metadata, payload } => { 135 | debug!( 136 | "{} metadata res from {}", 137 | peer.ctx.local_addr, peer.ctx.remote_addr 138 | ); 139 | debug!("{metadata:?}"); 140 | 141 | let peer_ext_id = peer.extension.metadata_size.unwrap(); 142 | 143 | peer.torrent_ctx 144 | .tx 145 | .send(TorrentMsg::DownloadedInfoPiece( 146 | peer_ext_id, 147 | metadata.piece, 148 | payload.clone(), 149 | )) 150 | .await?; 151 | peer.torrent_ctx 152 | .tx 153 | .send(TorrentMsg::SendCancelMetadata { 154 | from: peer.ctx.id, 155 | index: metadata.piece, 156 | }) 157 | .await?; 158 | } 159 | Metadata::Request(piece) => { 160 | debug!( 161 | "{} metadata req from {}", 162 | peer.ctx.local_addr, peer.ctx.remote_addr 163 | ); 164 | debug!("piece = {piece:?}"); 165 | 166 | let (tx, rx) = oneshot::channel(); 167 | peer.torrent_ctx 168 | .tx 169 | .send(TorrentMsg::RequestInfoPiece(*piece, tx)) 170 | .await?; 171 | 172 | match rx.await? { 173 | Some(info_slice) => { 174 | info!("sending data with piece {:?}", piece); 175 | let payload = MetadataDict::data(*piece, &info_slice)?; 176 | sink.send(Core::Extended(Self::ID, payload).into()) 177 | .await; 178 | } 179 | None => { 180 | info!("sending reject"); 181 | let r = MetadataDict::reject(*piece) 182 | .to_bencode() 183 | .map_err(|_| Error::BencodeError)?; 184 | sink.send(Core::Extended(Self::ID, r).into()).await; 185 | } 186 | } 187 | } 188 | Metadata::Reject(piece) => { 189 | debug!( 190 | "{} metadata res from {}", 191 | peer.ctx.local_addr, peer.ctx.remote_addr 192 | ); 193 | debug!("piece = {piece:?}"); 194 | } 195 | } 196 | Ok(()) 197 | } 198 | 199 | fn is_supported( 200 | &self, 201 | extension: &crate::extensions::extended::Extension, 202 | ) -> bool { 203 | extension.m.ut_metadata.is_some() 204 | } 205 | 206 | fn codec(&self) -> Self::Codec { 207 | MetadataCodec 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types for the Metadata protocol codec. 2 | //! 3 | //! 4 | 5 | pub mod codec; 6 | 7 | use bendy::{ 8 | decoding::{self, FromBencode, Object, ResultExt}, 9 | encoding::ToBencode, 10 | }; 11 | 12 | use super::super::error; 13 | 14 | /// Metadata dict used in the Metadata protocol messages, 15 | /// this dict is used to request, reject, and send data (info). 16 | /// 17 | /// # Important 18 | /// 19 | /// Since the Metadata codec is handling [`codec::Metadata`] Reject and 20 | /// Request branches with just the [`Metadata::piece`], this is only used in the 21 | /// [`codec::Metadata::Response`] branch. 22 | #[derive(Debug, Clone, PartialEq)] 23 | pub struct Metadata { 24 | pub msg_type: MetadataMsgType, 25 | pub piece: u32, 26 | pub total_size: Option, 27 | } 28 | 29 | impl TryInto> for Metadata { 30 | type Error = bendy::encoding::Error; 31 | 32 | fn try_into(self) -> Result, Self::Error> { 33 | self.to_bencode() 34 | } 35 | } 36 | 37 | #[repr(u8)] 38 | #[derive(Copy, Clone, Debug, PartialEq)] 39 | pub enum MetadataMsgType { 40 | Request = 0, 41 | Response = 1, 42 | Reject = 2, 43 | } 44 | 45 | impl TryFrom for MetadataMsgType { 46 | type Error = error::Error; 47 | 48 | fn try_from(value: u8) -> Result { 49 | use MetadataMsgType::*; 50 | match value { 51 | v if v == Request as u8 => Ok(Request), 52 | v if v == Response as u8 => Ok(Response), 53 | v if v == Reject as u8 => Ok(Reject), 54 | _ => Err(error::Error::BencodeError), 55 | } 56 | } 57 | } 58 | 59 | impl Metadata { 60 | pub fn request(piece: u32) -> Self { 61 | Self { msg_type: MetadataMsgType::Request, piece, total_size: None } 62 | } 63 | 64 | pub fn data(piece: u32, info: &[u8]) -> Result, error::Error> { 65 | let metadata = Self { 66 | msg_type: MetadataMsgType::Response, 67 | piece, 68 | total_size: Some(info.len() as u32), 69 | }; 70 | 71 | let mut bytes = 72 | metadata.to_bencode().map_err(|_| error::Error::BencodeError)?; 73 | 74 | bytes.extend_from_slice(info); 75 | 76 | Ok(bytes) 77 | } 78 | 79 | pub fn reject(piece: u32) -> Self { 80 | Self { msg_type: MetadataMsgType::Reject, piece, total_size: None } 81 | } 82 | 83 | /// Tries to extract Info from the given buffer. 84 | /// 85 | /// # Errors 86 | /// 87 | /// This function will return an error if the buffer is not a valid Data 88 | /// type of the metadata extension protocol 89 | pub fn extract(mut buf: Vec) -> Result<(Self, Vec), error::Error> { 90 | // let mut info_buf = Vec::new(); 91 | let mut metadata_buf = Vec::new(); 92 | 93 | // find end of info dict, which is always the first "ee" 94 | if let Some(i) = buf.windows(2).position(|w| w == b"ee") { 95 | metadata_buf = buf.drain(..i + 2).collect(); 96 | } 97 | 98 | let metadata = Metadata::from_bencode(&metadata_buf) 99 | .map_err(|_| error::Error::BencodeError)?; 100 | 101 | Ok((metadata, buf)) 102 | } 103 | } 104 | 105 | impl FromBencode for Metadata { 106 | fn decode_bencode_object(object: Object) -> Result 107 | where 108 | Self: Sized, 109 | { 110 | let mut msg_type = 0; 111 | let mut piece = 0; 112 | let mut total_size = None; 113 | 114 | let mut dict_dec = object.try_into_dictionary()?; 115 | 116 | while let Some(pair) = dict_dec.next_pair()? { 117 | match pair { 118 | (b"msg_type", value) => { 119 | msg_type = 120 | u8::decode_bencode_object(value).context("msg_type")?; 121 | } 122 | (b"piece", value) => { 123 | piece = 124 | u32::decode_bencode_object(value).context("piece")?; 125 | } 126 | (b"total_size", value) => { 127 | total_size = u32::decode_bencode_object(value) 128 | .context("total_size") 129 | .map(Some)?; 130 | } 131 | _ => {} 132 | } 133 | } 134 | 135 | // Check that we discovered all necessary fields 136 | Ok(Self { msg_type: msg_type.try_into()?, piece, total_size }) 137 | } 138 | } 139 | 140 | impl ToBencode for Metadata { 141 | const MAX_DEPTH: usize = 20; 142 | 143 | fn encode( 144 | &self, 145 | encoder: bendy::encoding::SingleItemEncoder, 146 | ) -> Result<(), bendy::encoding::Error> { 147 | encoder.emit_dict(|mut e| { 148 | e.emit_pair(b"msg_type", self.msg_type as u8)?; 149 | e.emit_pair(b"piece", self.piece)?; 150 | if let Some(total_size) = self.total_size { 151 | e.emit_pair(b"total_size", total_size)?; 152 | }; 153 | Ok(()) 154 | })?; 155 | Ok(()) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /crates/vincenzo/src/extensions/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extensions (protocols) that act on Peers, including the core protocol. 2 | 3 | pub mod core; 4 | pub mod extended; 5 | pub mod metadata; 6 | -------------------------------------------------------------------------------- /crates/vincenzo/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for working with the BitTorrent protocol V1. 2 | //! 3 | //! This is the library created for Vincenzo, a BitTorrent client. It uses this 4 | //! library to create both the daemon and ui binaries. 5 | //! 6 | //! This crate contains building blocks for developing software using this 7 | //! protocol in a high-level manner. 8 | //! 9 | //! A few ideas that benefit from a distributed and decentralized protocol: 10 | //! 11 | //! * A program that synchronizes files between multiple peers. 12 | //! * A fully encrypted chat client with files. 13 | //! 14 | //! # Example 15 | //! 16 | //! This is how you can download torrents using just the daemon, 17 | //! we simply run the [daemon] and send messages to it. 18 | //! 19 | //! ``` 20 | //! use vincenzo::daemon::Daemon; 21 | //! use vincenzo::daemon::DaemonMsg; 22 | //! use vincenzo::magnet::Magnet; 23 | //! use tokio::spawn; 24 | //! use tokio::sync::oneshot; 25 | //! 26 | //! #[tokio::main] 27 | //! async fn main() { 28 | //! let download_dir = "/home/gabriel/Downloads".to_string(); 29 | //! 30 | //! let mut daemon = Daemon::new(download_dir); 31 | //! let tx = daemon.ctx.tx.clone(); 32 | //! 33 | //! spawn(async move { 34 | //! daemon.run().await.unwrap(); 35 | //! }); 36 | //! 37 | //! let magnet = Magnet::new("magnet:?xt=urn:btih:ab6ad7ff24b5ed3a61352a1f1a7811a8c3cc6dde&dn=archlinux-2023.09.01-x86_64.iso").unwrap(); 38 | //! 39 | //! // identifier of the torrent 40 | //! let info_hash = magnet.parse_xt(); 41 | //! 42 | //! tx.send(DaemonMsg::NewTorrent(magnet)).await.unwrap(); 43 | //! 44 | //! // get information about the torrent download 45 | //! let (otx, orx) = oneshot::channel(); 46 | //! 47 | //! tx.send(DaemonMsg::RequestTorrentState(info_hash, otx)).await.unwrap(); 48 | //! let torrent_state = orx.await.unwrap(); 49 | //! 50 | //! // TorrentState { 51 | //! // name: "torrent name", 52 | //! // download_rate: 999999, 53 | //! // ... 54 | //! // } 55 | //! } 56 | //! ``` 57 | 58 | #![feature(macro_metavar_expr)] 59 | 60 | pub mod args; 61 | pub mod avg; 62 | pub mod bitfield; 63 | pub mod config; 64 | pub mod counter; 65 | pub mod daemon; 66 | pub mod daemon_wire; 67 | pub mod disk; 68 | pub mod error; 69 | pub mod extensions; 70 | pub mod magnet; 71 | pub mod metainfo; 72 | pub mod peer; 73 | pub mod torrent; 74 | pub mod tracker; 75 | pub mod utils; 76 | -------------------------------------------------------------------------------- /crates/vincenzo/src/magnet.rs: -------------------------------------------------------------------------------- 1 | //! Handle magnet link 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use magnet_url::Magnet as Magnet_; 5 | 6 | use crate::{error::Error, torrent::InfoHash}; 7 | 8 | #[derive(Debug, Clone, Hash)] 9 | pub struct Magnet(Magnet_); 10 | 11 | impl Deref for Magnet { 12 | type Target = Magnet_; 13 | fn deref(&self) -> &Self::Target { 14 | &self.0 15 | } 16 | } 17 | 18 | impl DerefMut for Magnet { 19 | fn deref_mut(&mut self) -> &mut Self::Target { 20 | &mut self.0 21 | } 22 | } 23 | 24 | impl Magnet { 25 | pub fn new(magnet_url: &str) -> Result { 26 | Ok(Self( 27 | Magnet_::new(magnet_url).map_err(|_| Error::MagnetLinkInvalid)?, 28 | )) 29 | } 30 | 31 | /// The name will come URL encoded, and it is also optional. 32 | pub fn parse_dn(&self) -> String { 33 | if let Some(dn) = self.dn.clone() { 34 | if let Ok(dn) = urlencoding::decode(&dn) { 35 | return dn.to_string(); 36 | } 37 | } 38 | "Unknown".to_string() 39 | } 40 | 41 | /// Transform the "xt" field from hex, to a slice. 42 | pub fn parse_xt(&self) -> [u8; 20] { 43 | let info_hash = hex::decode(self.xt.clone().unwrap()).unwrap(); 44 | let mut x = [0u8; 20]; 45 | 46 | x[..20].copy_from_slice(&info_hash[..20]); 47 | x 48 | } 49 | 50 | /// Transform the "xt" field from hex, to a slice. 51 | pub fn parse_xt_infohash(&self) -> InfoHash { 52 | let info_hash = hex::decode(self.xt.clone().unwrap()).unwrap(); 53 | let mut x = [0u8; 20]; 54 | 55 | x[..20].copy_from_slice(&info_hash[..20]); 56 | x.into() 57 | } 58 | 59 | /// Parse trackers so they can be used as socket addresses. 60 | pub fn parse_trackers(&self) -> Vec { 61 | let tr: Vec = self 62 | .tr 63 | .clone() 64 | .iter_mut() 65 | .filter(|x| x.starts_with("udp")) 66 | .map(|x| { 67 | *x = urlencoding::decode(x).unwrap().to_string(); 68 | *x = x.replace("udp://", ""); 69 | 70 | // remove any /announce 71 | if let Some(i) = x.find('/') { 72 | *x = x[..i].to_string(); 73 | }; 74 | 75 | x.to_owned() 76 | }) 77 | .collect(); 78 | tr 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | pub mod tests { 84 | #[test] 85 | fn parse_string_to_magnet() { 86 | let mstr = "magnet:?xt=urn:btih:56BC861F42972DEA863AE853362A20E15C7BA07E&dn=Rust%20for%20Rustaceans%3A%20Idiomatic%20Programming&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Ftracker.bittor.pw%3A1337%2Fannounce&tr=udp%3A%2F%2Fpublic.popcorn-tracker.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Fopen.demonii.com%3A1337%2Fannounce"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/vincenzo/src/peer/session.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use tokio::time::Instant; 4 | 5 | use crate::{avg::SlidingDurationAvg, counter::ThruputCounters}; 6 | 7 | /// At any given time, a connection with a peer is in one of the below states. 8 | #[derive(Clone, Default, Copy, Debug, PartialEq)] 9 | pub enum ConnectionState { 10 | /// The peer connection has not yet been connected or it had been connected 11 | /// before but has been stopped. 12 | #[default] 13 | Disconnected, 14 | /// The state during which the TCP connection is established. 15 | Connecting, 16 | /// The state after establishing the TCP connection and exchanging the 17 | /// initial BitTorrent handshake. 18 | Handshaking, 19 | // This state is optional, it is used to verify that the bitfield exchange 20 | // occurrs after the handshake and not later. It is set once the 21 | // handshakes are exchanged and changed as soon as we receive the 22 | // bitfield or the the first message that is not a bitfield. Any 23 | // subsequent bitfield messages are rejected and the connection is 24 | // dropped, as per the standard. AvailabilityExchange, 25 | /// This is the normal state of a peer session, in which any messages, 26 | /// apart from the 'handshake' and 'bitfield', may be exchanged. 27 | Connected, 28 | /// This state is set when the program is gracefully shutting down, 29 | /// In this state, we don't send the outgoing blocks to the tracker on 30 | /// shutdown. 31 | Quitting, 32 | } 33 | 34 | /// Contains the state of both sides of the connection. 35 | #[derive(Clone, Copy, Debug)] 36 | pub struct State { 37 | /// The current state of the connection. 38 | pub connection: ConnectionState, 39 | /// If we're choked, peer doesn't allow us to download pieces from them. 40 | pub am_choking: bool, 41 | /// If we're interested, peer has pieces that we don't have. 42 | pub am_interested: bool, 43 | /// If peer is choked, we don't allow them to download pieces from us. 44 | pub peer_choking: bool, 45 | /// If peer is interested in us, they mean to download pieces that we have. 46 | pub peer_interested: bool, 47 | // when the torrent is paused, those values will be set, so we can 48 | // assign them again when the torrent is resumed. 49 | // peer interested will be calculated by parsing the peers pieces 50 | pub prev_peer_choking: bool, 51 | } 52 | 53 | impl Default for State { 54 | /// By default, both sides of the connection start off as choked and not 55 | /// interested in the other. 56 | fn default() -> Self { 57 | Self { 58 | connection: Default::default(), 59 | am_choking: true, 60 | am_interested: false, 61 | peer_choking: true, 62 | peer_interested: false, 63 | prev_peer_choking: true, 64 | } 65 | } 66 | } 67 | 68 | /// Holds and provides facilities to modify the state of a peer session. 69 | #[derive(Debug)] 70 | pub struct Session { 71 | /// The session state. 72 | pub state: State, 73 | /// Measures various transfer statistics. 74 | pub counters: ThruputCounters, 75 | /// Whether we're in endgame mode. 76 | pub in_endgame: bool, 77 | /// The target request queue size is the number of block requests we keep 78 | /// outstanding 79 | pub target_request_queue_len: u16, 80 | /// The last time some requests were sent to the peer. 81 | pub last_outgoing_request_time: Option, 82 | /// Updated with the time of receipt of the most recently received 83 | /// requested block. 84 | pub last_incoming_block_time: Option, 85 | /// Updated with the time of receipt of the most recently uploaded block. 86 | pub last_outgoing_block_time: Option, 87 | /// This is the average network round-trip-time between the last issued 88 | /// a request and receiving the next block. 89 | /// 90 | /// Note that it doesn't have to be the same block since peers are not 91 | /// required to serve our requests in order, so this is more of a general 92 | /// approximation. 93 | pub avg_request_rtt: SlidingDurationAvg, 94 | pub request_timed_out: bool, 95 | pub timed_out_request_count: usize, 96 | /// The time the BitTorrent connection was established (i.e. after 97 | /// handshaking) 98 | pub connected_time: Option, 99 | /// If the torrent was fully downloaded, all peers will become seed only. 100 | /// They will only seed but not download anything anymore. 101 | pub seed_only: bool, 102 | } 103 | 104 | impl Default for Session { 105 | fn default() -> Self { 106 | Self { 107 | state: State::default(), 108 | counters: ThruputCounters::default(), 109 | in_endgame: false, 110 | target_request_queue_len: Session::DEFAULT_REQUEST_QUEUE_LEN, 111 | connected_time: None, 112 | avg_request_rtt: SlidingDurationAvg::default(), 113 | request_timed_out: false, 114 | timed_out_request_count: 0, 115 | last_incoming_block_time: None, 116 | last_outgoing_block_time: None, 117 | last_outgoing_request_time: None, 118 | seed_only: false, 119 | } 120 | } 121 | } 122 | 123 | impl Session { 124 | /// The value of outstanding blocks for a peer. 125 | /// 126 | /// Before we do an extended handshake, 127 | /// we do not have access to `reqq`. 128 | /// And so this value is initialized with a sane default, 129 | /// most clients support 250+ inflight requests. 130 | /// 131 | /// After the extended handshake, this value is not used 132 | /// in favour of the `reqq`, if the peer has it. 133 | pub const DEFAULT_REQUEST_QUEUE_LEN: u16 = 150; 134 | 135 | /// The smallest timeout value we can give a peer. Very fast peers will have 136 | /// an average round-trip-times, so a slight deviation would punish them 137 | /// unnecessarily. Therefore we use a somewhat larger minimum threshold for 138 | /// timeouts. 139 | const MIN_TIMEOUT: Duration = Duration::from_secs(2); 140 | 141 | /// Returns the current request timeout value, based on the running average 142 | /// of past request round trip times. 143 | pub fn request_timeout(&self) -> Duration { 144 | // we allow up to four times the average deviation from the mean 145 | // let t = self.avg_request_rtt.mean() + 4 * 146 | // self.avg_request_rtt.deviation(); t.max(Self::MIN_TIMEOUT) 147 | Self::MIN_TIMEOUT 148 | } 149 | 150 | /// Updates state to reflect that peer was timed out. 151 | pub fn register_request_timeout(&mut self) { 152 | // peer has timed out, only allow a single outstanding request 153 | // from now until peer hasn't timed out 154 | // self.target_request_queue_len -= 1; 155 | self.timed_out_request_count += 1; 156 | self.request_timed_out = true; 157 | } 158 | 159 | /// Updates various statistics around a block download. 160 | /// This should be called every time a block is received. 161 | pub fn update_download_stats(&mut self, block_len: u32) { 162 | let now = Instant::now(); 163 | 164 | // update request time 165 | if let Some(last_outgoing_request_time) = 166 | &mut self.last_outgoing_request_time 167 | { 168 | // Due to what is presumed to be inconsistencies with the 169 | // `Instant::now()` API, it happens in rare circumstances that using 170 | // the regular `duration_since` here panics (#48). I suspect this 171 | // happens when requests are made a very short interval before this 172 | // function is called, which is likely in very fast downloads. 173 | // Either way, we guard against this by defaulting to 0. 174 | let elapsed_since_last_request = 175 | now.saturating_duration_since(*last_outgoing_request_time); 176 | 177 | // If we timed out before, check if this request arrived within the 178 | // timeout window, or outside of it. If it arrived within the 179 | // window, we can mark peer as having recovered from the timeout. 180 | if self.request_timed_out 181 | && elapsed_since_last_request <= self.request_timeout() 182 | { 183 | self.request_timed_out = false; 184 | } 185 | 186 | let request_rtt = elapsed_since_last_request; 187 | self.avg_request_rtt.update(request_rtt); 188 | } 189 | 190 | self.counters.payload.down += block_len as u64; 191 | self.last_incoming_block_time = Some(now); 192 | } 193 | 194 | pub fn record_waste(&mut self, block_len: u32) { 195 | self.counters.waste += block_len as u64; 196 | } 197 | 198 | pub fn update_upload_stats(&mut self, block_len: u32) { 199 | self.last_outgoing_block_time = Some(Instant::now()); 200 | self.counters.payload.up += block_len as u64; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /crates/vincenzo/src/tracker/action.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Action { 3 | Connect, 4 | Announce, 5 | Scrape, 6 | Unsupported, 7 | } 8 | 9 | impl From for u32 { 10 | fn from(a: Action) -> Self { 11 | match a { 12 | Action::Connect => 0, 13 | Action::Announce => 1, 14 | Action::Scrape => 2, 15 | Action::Unsupported => 0xffff, 16 | } 17 | } 18 | } 19 | 20 | impl From for Action { 21 | fn from(x: u32) -> Self { 22 | match x { 23 | 0 => Action::Connect, 24 | 1 => Action::Announce, 25 | 2 => Action::Scrape, 26 | _ => Action::Unsupported, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/vincenzo/src/tracker/announce.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use speedy::{BigEndian, Readable, Writable}; 3 | 4 | use crate::{error::Error, torrent::Stats}; 5 | 6 | use super::{action::Action, event::Event}; 7 | 8 | #[derive(Debug, PartialEq, Readable, Writable)] 9 | pub struct Request { 10 | pub connection_id: u64, 11 | pub action: u32, 12 | pub transaction_id: u32, 13 | pub info_hash: [u8; 20], 14 | pub peer_id: [u8; 20], 15 | pub downloaded: u64, 16 | pub left: u64, 17 | pub uploaded: u64, 18 | pub event: u64, 19 | pub ip_address: u32, 20 | pub num_want: u32, 21 | pub port: u16, 22 | } 23 | 24 | impl Request { 25 | pub const LENGTH: usize = 98; 26 | 27 | pub fn new( 28 | connection_id: u64, 29 | info_hash: [u8; 20], 30 | peer_id: [u8; 20], 31 | _ip_address: u32, 32 | port: u16, 33 | event: Event, 34 | ) -> Self { 35 | let mut rng = rand::thread_rng(); 36 | Self { 37 | connection_id, 38 | action: Action::Announce.into(), 39 | transaction_id: rng.gen(), 40 | info_hash, 41 | peer_id, 42 | downloaded: 0, 43 | left: u64::MAX, 44 | uploaded: 0, 45 | event: event.into(), 46 | ip_address: 0, 47 | num_want: u32::MAX, 48 | port, 49 | } 50 | } 51 | 52 | pub fn deserialize(buf: &[u8]) -> Result<(Self, &[u8]), Error> { 53 | if buf.len() != Self::LENGTH { 54 | return Err(Error::TrackerResponseLength); 55 | } 56 | 57 | let res = Self::read_from_buffer_with_ctx(BigEndian {}, buf)?; 58 | 59 | Ok((res, &buf[Self::LENGTH..])) 60 | } 61 | 62 | pub fn serialize(&self) -> Vec { 63 | self.write_to_vec_with_ctx(BigEndian {}).unwrap() 64 | } 65 | } 66 | 67 | #[derive(Debug, PartialEq, Writable, Readable)] 68 | pub struct Response { 69 | pub action: u32, 70 | pub transaction_id: u32, 71 | pub interval: u32, 72 | pub leechers: u32, 73 | pub seeders: u32, 74 | } 75 | 76 | impl From for Stats { 77 | fn from(value: Response) -> Self { 78 | Self { 79 | interval: value.interval, 80 | seeders: value.seeders, 81 | leechers: value.leechers, 82 | } 83 | } 84 | } 85 | 86 | impl Response { 87 | pub(crate) const LENGTH: usize = 20; 88 | 89 | pub fn deserialize(buf: &[u8]) -> Result<(Self, &[u8]), Error> { 90 | if buf.len() < Response::LENGTH { 91 | return Err(Error::TrackerResponseLength); 92 | } 93 | 94 | let res = Self::read_from_buffer_with_ctx(BigEndian {}, buf)?; 95 | 96 | Ok((res, &buf[Self::LENGTH..])) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/vincenzo/src/tracker/connect.rs: -------------------------------------------------------------------------------- 1 | use speedy::{BigEndian, Readable, Writable}; 2 | use tracing::debug; 3 | 4 | use crate::error::Error; 5 | 6 | use super::action::Action; 7 | 8 | #[derive(Debug, PartialEq, Clone, Readable, Writable)] 9 | pub struct Request { 10 | pub protocol_id: u64, 11 | pub action: u32, 12 | pub transaction_id: u32, 13 | } 14 | 15 | impl Default for Request { 16 | fn default() -> Self { 17 | Self::new() 18 | } 19 | } 20 | 21 | impl Request { 22 | pub(crate) const LENGTH: usize = 16; 23 | const MAGIC: u64 = 0x41727101980; 24 | 25 | pub fn new() -> Self { 26 | Self { 27 | protocol_id: Self::MAGIC, 28 | action: Action::Connect.into(), 29 | transaction_id: rand::random::(), 30 | } 31 | } 32 | 33 | pub fn serialize(&self) -> [u8; 16] { 34 | debug!("sending connect request {self:#?}"); 35 | let mut buf = [0u8; 16]; 36 | buf[..8].copy_from_slice(&Self::MAGIC.to_be_bytes()); 37 | buf[8..12].copy_from_slice(&self.action.to_be_bytes()); 38 | buf[12..16].copy_from_slice(&self.transaction_id.to_be_bytes()); 39 | buf 40 | } 41 | 42 | pub fn deserialize(buf: &[u8]) -> Result<(Self, &[u8]), Error> { 43 | if buf.len() != Self::LENGTH { 44 | return Err(Error::TrackerResponse); 45 | } 46 | 47 | let req = Self::read_from_buffer_with_ctx(BigEndian {}, buf) 48 | .map_err(Error::SpeedyError)?; 49 | 50 | Ok((req, &buf[Self::LENGTH..])) 51 | } 52 | } 53 | 54 | #[derive(Debug, PartialEq, Readable, Writable)] 55 | pub struct Response { 56 | pub action: u32, 57 | pub transaction_id: u32, 58 | pub connection_id: u64, 59 | } 60 | 61 | impl Response { 62 | pub(crate) const LENGTH: usize = 16; 63 | 64 | pub fn deserialize(buf: &[u8]) -> Result<(Self, &[u8]), Error> { 65 | if buf.len() != Self::LENGTH { 66 | return Err(Error::TrackerResponse); 67 | } 68 | 69 | let action = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); 70 | let transaction_id = 71 | u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); 72 | let connection_id = u64::from_be_bytes([ 73 | buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], 74 | buf[15], 75 | ]); 76 | 77 | Ok(( 78 | Self { action, transaction_id, connection_id }, 79 | &buf[Self::LENGTH..], 80 | )) 81 | } 82 | 83 | pub fn serialize(&self) -> Vec { 84 | self.write_to_vec_with_ctx(BigEndian {}).unwrap() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/vincenzo/src/tracker/event.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Default)] 2 | pub enum Event { 3 | #[default] 4 | None, 5 | Completed, 6 | Started, 7 | Stopped, 8 | } 9 | 10 | impl From for u64 { 11 | fn from(a: Event) -> Self { 12 | match a { 13 | Event::None => 0, 14 | Event::Completed => 1, 15 | Event::Started => 2, 16 | Event::Stopped => 3, 17 | } 18 | } 19 | } 20 | 21 | impl From for Event { 22 | fn from(x: u64) -> Self { 23 | match x { 24 | 0 => Event::None, 25 | 1 => Event::Completed, 26 | 2 => Event::Started, 27 | 3 => Event::Stopped, 28 | _ => Event::None, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/vincenzo/src/tracker/mod.rs: -------------------------------------------------------------------------------- 1 | //! A tracker is a server that manages peers and stats of multiple torrents. 2 | pub mod action; 3 | pub mod announce; 4 | pub mod connect; 5 | pub mod event; 6 | 7 | use super::tracker::action::Action; 8 | use std::{ 9 | fmt::Debug, 10 | net::{IpAddr, Ipv4Addr, SocketAddr}, 11 | time::Duration, 12 | }; 13 | 14 | use crate::error::Error; 15 | use rand::Rng; 16 | use tokio::{ 17 | net::{ToSocketAddrs, UdpSocket}, 18 | select, 19 | sync::{mpsc, oneshot}, 20 | time::timeout, 21 | }; 22 | use tracing::{debug, error, warn}; 23 | 24 | use self::event::Event; 25 | 26 | #[derive(Debug)] 27 | pub struct Tracker { 28 | /// UDP Socket of the `tracker_addr` 29 | /// Peers announcing will send handshakes 30 | /// to this addr 31 | pub local_addr: SocketAddr, 32 | pub peer_addr: SocketAddr, 33 | pub ctx: TrackerCtx, 34 | pub rx: mpsc::Receiver, 35 | } 36 | 37 | impl Default for Tracker { 38 | fn default() -> Self { 39 | let (tx, rx) = mpsc::channel::(300); 40 | 41 | let peer_id = Self::gen_peer_id(); 42 | 43 | Self { 44 | rx, 45 | local_addr: "0.0.0.0:0".parse().unwrap(), 46 | peer_addr: "0.0.0.0:0".parse().unwrap(), 47 | ctx: TrackerCtx { 48 | tx: tx.into(), 49 | peer_id, 50 | tracker_addr: "".to_owned(), 51 | connection_id: None, 52 | local_peer_addr: "0.0.0.0:0".parse().unwrap(), 53 | }, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | pub struct TrackerCtx { 60 | pub tx: Option>, 61 | /// Our ID for this connected Tracker 62 | pub peer_id: [u8; 20], 63 | /// UDP Socket of the `socket` in Tracker struct 64 | pub tracker_addr: String, 65 | /// Our peer socket addr, peers will send handshakes 66 | /// to this addr. 67 | pub local_peer_addr: SocketAddr, 68 | pub connection_id: Option, 69 | } 70 | 71 | impl Default for TrackerCtx { 72 | fn default() -> Self { 73 | TrackerCtx { 74 | local_peer_addr: "0.0.0.0:0".parse().unwrap(), 75 | peer_id: Tracker::gen_peer_id(), 76 | tx: None, 77 | connection_id: None, 78 | tracker_addr: "".to_owned(), 79 | } 80 | } 81 | } 82 | 83 | #[derive(Debug)] 84 | pub enum TrackerMsg { 85 | Announce { 86 | event: Event, 87 | info_hash: [u8; 20], 88 | downloaded: u64, 89 | uploaded: u64, 90 | left: u64, 91 | recipient: Option>>, 92 | }, 93 | } 94 | 95 | impl Tracker { 96 | const ANNOUNCE_RES_BUF_LEN: usize = 8192; 97 | 98 | pub fn new() -> Self { 99 | Self::default() 100 | } 101 | 102 | /// Bind UDP socket and send a connect handshake, 103 | /// to one of the trackers. 104 | // todo: get a new tracker if download is stale 105 | #[tracing::instrument(skip(trackers), name = "tracker::connect")] 106 | pub async fn connect(trackers: Vec) -> Result 107 | where 108 | A: ToSocketAddrs 109 | + Debug 110 | + Send 111 | + Sync 112 | + 'static 113 | + std::fmt::Display 114 | + Clone, 115 | A::Iter: Send, 116 | { 117 | debug!("...trying to connect to {:?} trackers", trackers.len()); 118 | 119 | // Connect to all trackers, return on the first 120 | // successful handshake. 121 | for tracker_addr in trackers { 122 | debug!("trying to connect {tracker_addr:?}"); 123 | 124 | let socket = match Self::new_udp_socket(tracker_addr.clone()).await 125 | { 126 | Ok(socket) => socket, 127 | Err(_) => { 128 | debug!("could not connect to tracker"); 129 | continue; 130 | } 131 | }; 132 | let (tracker_tx, tracker_rx) = mpsc::channel::(300); 133 | let mut tracker = Tracker { 134 | ctx: TrackerCtx { 135 | tracker_addr: tracker_addr.to_string(), 136 | tx: tracker_tx.into(), 137 | peer_id: Tracker::gen_peer_id(), 138 | local_peer_addr: "0.0.0.0:0".parse().unwrap(), 139 | connection_id: None, 140 | }, 141 | rx: tracker_rx, 142 | local_addr: socket.local_addr().unwrap(), 143 | peer_addr: socket.peer_addr().unwrap(), 144 | }; 145 | if tracker.connect_exchange(socket).await.is_ok() { 146 | debug!("announced to tracker {tracker_addr}"); 147 | return Ok(tracker); 148 | } 149 | } 150 | 151 | error!("Could not connect to any tracker, all trackers rejected the connection."); 152 | Err(Error::TrackerNoHosts) 153 | } 154 | 155 | #[tracing::instrument(skip(self))] 156 | async fn connect_exchange( 157 | &mut self, 158 | socket: UdpSocket, 159 | ) -> Result { 160 | let req = connect::Request::new(); 161 | let mut buf = [0u8; connect::Response::LENGTH]; 162 | let mut len: usize = 0; 163 | 164 | // will try to connect up to 3 times 165 | // breaking if succesfull 166 | for i in 0..=2 { 167 | debug!("sending connect number {i}..."); 168 | socket.send(&req.serialize()).await?; 169 | 170 | match timeout(Duration::new(5, 0), socket.recv(&mut buf)).await { 171 | Ok(Ok(lenn)) => { 172 | len = lenn; 173 | break; 174 | } 175 | Err(e) => { 176 | debug!("error receiving connect response, {e}"); 177 | } 178 | _ => {} 179 | } 180 | } 181 | 182 | if len == 0 { 183 | return Err(Error::TrackerResponse); 184 | } 185 | 186 | let (res, _) = connect::Response::deserialize(&buf)?; 187 | 188 | debug!("received res from tracker {res:#?}"); 189 | 190 | if res.transaction_id != req.transaction_id || res.action != req.action 191 | { 192 | error!("response is not valid {res:?}"); 193 | return Err(Error::TrackerResponse); 194 | } 195 | 196 | self.ctx.connection_id.replace(res.connection_id); 197 | Ok(socket) 198 | } 199 | 200 | /// Attempts to send an "announce_request" to the tracker 201 | #[tracing::instrument(skip(self, info_hash))] 202 | pub async fn announce_exchange( 203 | &mut self, 204 | info_hash: [u8; 20], 205 | listen: Option, 206 | ) -> Result<(announce::Response, Vec), Error> { 207 | let socket = UdpSocket::bind(self.local_addr).await?; 208 | socket.connect(self.peer_addr).await?; 209 | 210 | let connection_id = match self.ctx.connection_id { 211 | Some(x) => x, 212 | None => return Err(Error::TrackerNoConnectionId), 213 | }; 214 | 215 | let local_peer_socket = { 216 | match listen { 217 | Some(listen) => SocketAddr::new( 218 | IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 219 | listen.port(), 220 | ), 221 | None => { 222 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) 223 | } 224 | } 225 | }; 226 | 227 | let req = announce::Request::new( 228 | connection_id, 229 | info_hash, 230 | self.ctx.peer_id, 231 | // local_peer_socket.ip() as u32, 232 | 0, 233 | local_peer_socket.port(), 234 | Event::Started, 235 | ); 236 | 237 | debug!("announce req {req:?}"); 238 | 239 | self.ctx.local_peer_addr = local_peer_socket; 240 | 241 | debug!("local_peer_addr {:?}", self.ctx.local_peer_addr); 242 | 243 | debug!("local ip is {}", socket.local_addr()?); 244 | 245 | let mut len = 0_usize; 246 | let mut res = [0u8; Self::ANNOUNCE_RES_BUF_LEN]; 247 | 248 | // will try to connect up to 3 times 249 | // breaking if succesfull 250 | for i in 0..=2 { 251 | debug!("trying to send announce number {i}..."); 252 | socket.send(&req.serialize()).await?; 253 | match timeout(Duration::new(3, 0), socket.recv(&mut res)).await { 254 | Ok(Ok(lenn)) => { 255 | len = lenn; 256 | break; 257 | } 258 | Err(e) => { 259 | warn!("failed to announce {e:#?}"); 260 | } 261 | _ => {} 262 | } 263 | } 264 | 265 | if len == 0 { 266 | return Err(Error::TrackerResponse); 267 | } 268 | 269 | let res = &res[..len]; 270 | 271 | // res is the deserialized struct, 272 | // payload is a byte array of peers, 273 | // which are in the form of ips and ports 274 | let (res, payload) = announce::Response::deserialize(res)?; 275 | 276 | if res.transaction_id != req.transaction_id || res.action != req.action 277 | { 278 | return Err(Error::TrackerResponse); 279 | } 280 | 281 | debug!("* announce successful"); 282 | debug!("res from announce {:#?}", res); 283 | 284 | let peers = Self::parse_compact_peer_list( 285 | payload, 286 | socket.peer_addr()?.is_ipv6(), 287 | )?; 288 | 289 | Ok((res, peers)) 290 | } 291 | 292 | /// Connect is the first step in getting the file 293 | /// Create an UDP Socket for the given tracker address 294 | #[tracing::instrument(skip(addr))] 295 | pub async fn new_udp_socket( 296 | addr: A, 297 | ) -> Result { 298 | let socket = UdpSocket::bind("0.0.0.0:0").await; 299 | if let Ok(socket) = socket { 300 | if socket.connect(addr).await.is_ok() { 301 | return Ok(socket); 302 | } 303 | return Err(Error::TrackerSocketConnect); 304 | } 305 | Err(Error::TrackerSocketAddr) 306 | } 307 | 308 | #[tracing::instrument(skip(buf, is_ipv6))] 309 | fn parse_compact_peer_list( 310 | buf: &[u8], 311 | is_ipv6: bool, 312 | ) -> Result, Error> { 313 | let mut peer_list = Vec::::new(); 314 | 315 | // in ipv4 the addresses come in packets of 6 bytes, 316 | // first 4 for ip and 2 for port 317 | // in ipv6 its 16 bytes for port and 2 for port 318 | let stride = if is_ipv6 { 18 } else { 6 }; 319 | 320 | let chunks = buf.chunks_exact(stride); 321 | if !chunks.remainder().is_empty() { 322 | return Err(Error::TrackerCompactPeerList); 323 | } 324 | 325 | for hostpost in chunks { 326 | let (ip, port) = hostpost.split_at(stride - 2); 327 | let ip = if is_ipv6 { 328 | let octets: [u8; 16] = ip[0..16] 329 | .try_into() 330 | .expect("iterator guarantees bounds are OK"); 331 | IpAddr::from(std::net::Ipv6Addr::from(octets)) 332 | } else { 333 | IpAddr::from(std::net::Ipv4Addr::new( 334 | ip[0], ip[1], ip[2], ip[3], 335 | )) 336 | }; 337 | 338 | let port = u16::from_be_bytes( 339 | port.try_into().expect("iterator guarantees bounds are OK"), 340 | ); 341 | 342 | peer_list.push((ip, port).into()); 343 | } 344 | 345 | debug!("ips of peers addrs {peer_list:#?}"); 346 | let peers: Vec = peer_list.into_iter().collect(); 347 | 348 | Ok(peers) 349 | } 350 | #[tracing::instrument(skip(self))] 351 | pub async fn announce_msg( 352 | &self, 353 | event: Event, 354 | info_hash: [u8; 20], 355 | downloaded: u64, 356 | uploaded: u64, 357 | left: u64, 358 | ) -> Result { 359 | debug!("announcing {event:#?} to tracker"); 360 | let socket = UdpSocket::bind(self.local_addr).await?; 361 | socket.connect(self.peer_addr).await?; 362 | 363 | let req = announce::Request { 364 | connection_id: self.ctx.connection_id.unwrap_or(0), 365 | action: Action::Announce.into(), 366 | transaction_id: rand::thread_rng().gen(), 367 | info_hash, 368 | peer_id: self.ctx.peer_id, 369 | downloaded, 370 | left, 371 | uploaded, 372 | event: event.into(), 373 | ip_address: 0, 374 | num_want: u32::MAX, 375 | port: self.local_addr.port(), 376 | }; 377 | 378 | let mut len = 0_usize; 379 | let mut res = [0u8; Self::ANNOUNCE_RES_BUF_LEN]; 380 | 381 | // will try to connect up to 3 times 382 | // breaking if succesfull 383 | for _ in 0..=2 { 384 | socket.send(&req.serialize()).await?; 385 | match timeout(Duration::new(3, 0), socket.recv(&mut res)).await { 386 | Ok(Ok(lenn)) => { 387 | len = lenn; 388 | break; 389 | } 390 | Err(e) => { 391 | warn!("failed to announce {e:#?}"); 392 | } 393 | _ => {} 394 | } 395 | } 396 | 397 | let res = &res[..len]; 398 | 399 | let (res, _) = announce::Response::deserialize(res)?; 400 | 401 | Ok(res) 402 | } 403 | 404 | #[tracing::instrument(skip(self))] 405 | pub async fn run(&mut self) -> Result<(), Error> { 406 | debug!("running tracker"); 407 | loop { 408 | select! { 409 | Some(msg) = self.rx.recv() => { 410 | match msg { 411 | TrackerMsg::Announce { 412 | info_hash, 413 | downloaded, 414 | uploaded, 415 | recipient, 416 | event, 417 | left, 418 | } => { 419 | let r = self 420 | .announce_msg(event.clone(), info_hash, downloaded, uploaded, left) 421 | .await; 422 | 423 | if let Some(recipient) = recipient { 424 | let _ = recipient.send(r); 425 | } 426 | 427 | if event == Event::Stopped { 428 | return Ok(()); 429 | } 430 | } 431 | } 432 | } 433 | } 434 | } 435 | } 436 | /// Peer ids should be prefixed with "vcz". 437 | pub fn gen_peer_id() -> [u8; 20] { 438 | let mut peer_id = [0; 20]; 439 | peer_id[..3].copy_from_slice(b"vcz"); 440 | peer_id[3..].copy_from_slice(&rand::random::<[u8; 17]>()); 441 | peer_id 442 | } 443 | } 444 | 445 | #[cfg(test)] 446 | mod tests { 447 | use super::Tracker; 448 | 449 | #[test] 450 | fn peer_ids_prefixed_with_vcz() { 451 | // Poor man's fuzzing. 452 | let peer_id = Tracker::gen_peer_id(); 453 | for _ in 0..10 { 454 | assert!(peer_id.starts_with(&[b'v', b'c', b'z'])); 455 | } 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /crates/vincenzo/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions 2 | /// transform bytes into a human readable format. 3 | pub fn to_human_readable(mut n: f64) -> String { 4 | let units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; 5 | let delimiter = 1024_f64; 6 | if n < delimiter { 7 | return format!("{} {}", n, "B"); 8 | } 9 | let mut u: i32 = 0; 10 | let r = 10_f64; 11 | while (n * r).round() / r >= delimiter && u < (units.len() as i32) - 1 { 12 | n /= delimiter; 13 | u += 1; 14 | } 15 | format!("{:.2} {}", n, units[u as usize]) 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | pub fn readable_size() { 24 | let n = 495353_f64; 25 | assert_eq!(to_human_readable(n), "483.74 KiB"); 26 | 27 | let n = 30_178_876_f64; 28 | assert_eq!(to_human_readable(n), "28.78 MiB"); 29 | 30 | let n = 2093903856_f64; 31 | assert_eq!(to_human_readable(n), "1.95 GiB"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/vincenzo/tape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/crates/vincenzo/tape.gif -------------------------------------------------------------------------------- /crates/vincenzo/tests/integration.rs: -------------------------------------------------------------------------------- 1 | // use std::{fs::create_dir_all, net::SocketAddr, time::Duration}; 2 | // 3 | // use bitvec::{bitvec, prelude::Msb0}; 4 | // use futures::{SinkExt, StreamExt}; 5 | // use rand::{distributions::Alphanumeric, Rng}; 6 | // use tokio::{ 7 | // fs::OpenOptions, 8 | // io::AsyncWriteExt, 9 | // net::{TcpListener, TcpStream}, 10 | // select, spawn, 11 | // sync::mpsc, 12 | // time::interval, 13 | // }; 14 | // use tracing::debug; 15 | // use vincenzo::{ 16 | // daemon::DaemonMsg, 17 | // disk::{Disk, DiskMsg}, 18 | // magnet::Magnet, 19 | // metainfo::Info, 20 | // peer::{Direction, Peer, PeerMsg}, 21 | // tcp_wire::{messages::Message, Block, BlockInfo}, 22 | // torrent::{Torrent, TorrentMsg}, 23 | // tracker::Tracker, 24 | // }; 25 | 26 | // Test that a peer will re-request block_infos after timeout, 27 | // this test will spawn a tracker-less torrent and simulate 2 peers 28 | // communicating with each other, a seeder and a leecher. 29 | // 30 | // The leecher will request one block, the seeder will not answer, 31 | // and then the leecher must send the request again. 32 | // 33 | // todo: this is extremely verbose to setup, maybe testing 2 34 | // different daemons running would be easier. 35 | // #[tokio::test] 36 | // async fn peer_request() { 37 | // tracing_subscriber::fmt() 38 | // .with_env_filter("tokio=trace,runtime=trace") 39 | // .with_max_level(tracing::Level::DEBUG) 40 | // .with_target(false) 41 | // .compact() 42 | // .with_file(false) 43 | // .without_time() 44 | // .init(); 45 | // 46 | // let original_hook = std::panic::take_hook(); 47 | // 48 | // let mut rng = rand::thread_rng(); 49 | // let name: String = (0..20).map(|_| rng.sample(Alphanumeric) as 50 | // char).collect(); let download_dir: String = (0..20).map(|_| 51 | // rng.sample(Alphanumeric) as char).collect(); let info_hash = [0u8; 20]; 52 | // let local_peer_id = Tracker::gen_peer_id(); 53 | // let download_dir_2 = download_dir.clone(); 54 | // 55 | // std::panic::set_hook(Box::new(move |panic| { 56 | // let _ = std::fs::remove_dir_all(&download_dir_2); 57 | // original_hook(panic); 58 | // })); 59 | // 60 | // create_dir_all(download_dir.clone()).unwrap(); 61 | // 62 | // let mut file = OpenOptions::new() 63 | // .read(true) 64 | // .write(true) 65 | // .create(true) 66 | // .open(format!("{download_dir}/{name}")) 67 | // .await 68 | // .unwrap(); 69 | // 70 | // let bytes = [3u8; 30_usize]; 71 | // file.write_all(&bytes).await.unwrap(); 72 | // 73 | // let magnet = 74 | // format!("magnet:?xt=urn:btih:9999999999999999999999999999999999999999& 75 | // dn={name}&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce& 76 | // tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F% 77 | // 2Ftracker.bittor.pw%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr. 78 | // org%3A1337&tr=udp%3A%2F%2Fbt.xxx-tracker.com%3A2710%2Fannounce& 79 | // tr=udp%3A%2F%2Fpublic.popcorn-tracker.org%3A6969%2Fannounce&tr=udp%3A%2F% 80 | // 2Feddie4.nl%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org% 81 | // 3A451%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce& 82 | // tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F% 83 | // 2Fopen.stealth.si%3A80%2Fannounce"); let info = Info { 84 | // file_length: Some(30), 85 | // name, 86 | // piece_length: 15, 87 | // pieces: vec![0; 40], 88 | // files: None, 89 | // }; 90 | // 91 | // let magnet = Magnet::new(&magnet).unwrap(); 92 | // let (daemon_tx, _daemon_rx) = mpsc::channel::(1000); 93 | // 94 | // let (disk_tx, disk_rx) = mpsc::channel::(1000); 95 | // let mut disk = Disk::new(disk_rx, download_dir.clone()); 96 | // 97 | // let mut torrent = Torrent::new(disk_tx.clone(), daemon_tx.clone(), 98 | // magnet.clone()); torrent.stats.seeders = 1; 99 | // torrent.stats.leechers = 1; 100 | // torrent.size = info.get_size(); 101 | // torrent.have_info = true; 102 | // torrent 103 | // .ctx 104 | // .has_at_least_one_piece 105 | // .store(true, std::sync::atomic::Ordering::Relaxed); 106 | // 107 | // // pretend we already have the info, 108 | // // that was downloaded from the magnet 109 | // let mut torrent_info = torrent.ctx.info.write().await; 110 | // *torrent_info = info.clone(); 111 | // drop(torrent_info); 112 | // 113 | // let mut p = torrent.ctx.bitfield.write().await; 114 | // *p = bitvec![u8, Msb0; 0; info.pieces() as usize]; 115 | // drop(p); 116 | // 117 | // disk.new_torrent(torrent.ctx.clone()).await.unwrap(); 118 | // 119 | // spawn(async move { 120 | // disk.run().await.unwrap(); 121 | // }); 122 | // 123 | // let seeder: SocketAddr = "127.0.0.1:3333".parse().unwrap(); 124 | // let torrent_ctx = torrent.ctx.clone(); 125 | // 126 | // spawn(async move { 127 | // torrent.run().await.unwrap(); 128 | // }); 129 | // 130 | // let listener = TcpListener::bind(seeder).await.unwrap(); 131 | // 132 | // spawn(async move { 133 | // let torrent_ctx = torrent_ctx.clone(); 134 | // loop { 135 | // if let Ok((socket, remote)) = listener.accept().await { 136 | // let local = socket.local_addr().unwrap(); 137 | // 138 | // let (socket, handshake) = 139 | // Peer::handshake(socket, Direction::Inbound, info_hash, 140 | // local_peer_id) .await 141 | // .unwrap(); 142 | // 143 | // let mut peer = Peer::new(remote, torrent_ctx.clone(), 144 | // handshake, local); peer.session.state.peer_choking = false; 145 | // peer.session.state.am_interested = false; 146 | // peer.have_info = true; 147 | // 148 | // let mut tick_timer = interval(Duration::from_secs(1)); 149 | // 150 | // let _ = peer 151 | // .torrent_ctx 152 | // .tx 153 | // .send(TorrentMsg::PeerConnected(peer.ctx.id, 154 | // peer.ctx.clone())) .await; 155 | // 156 | // let (mut sink, mut stream) = socket.split(); 157 | // 158 | // let mut n = 0; 159 | // 160 | // loop { 161 | // select! { 162 | // _ = tick_timer.tick(), if peer.have_info => { 163 | // peer.tick(&mut sink).await.unwrap(); 164 | // } 165 | // Some(Ok(msg)) = stream.next() => { 166 | // match msg { 167 | // Message::Request(block) => { 168 | // debug!("{local} received \n {block:#?} \n 169 | // from {remote}"); 170 | // 171 | // // only answer on the second request 172 | // if n == 1 { 173 | // let b: [u8; 15] = rand::random(); 174 | // sink.send(Message::Piece(Block { 175 | // index: 0, 176 | // begin: 0, 177 | // block: b.into(), 178 | // })) 179 | // .await.unwrap(); 180 | // } 181 | // if n == 2 { 182 | // let b: [u8; 15] = rand::random(); 183 | // sink.send(Message::Piece(Block { 184 | // index: 1, 185 | // begin: 0, 186 | // block: b.into(), 187 | // })) 188 | // .await.unwrap(); 189 | // } 190 | // 191 | // n += 1; 192 | // } 193 | // Message::Bitfield(field) => { 194 | // debug!("{local} bitfield {field:?} 195 | // {remote}"); } 196 | // _ => {} 197 | // } 198 | // } 199 | // } 200 | // } 201 | // } 202 | // } 203 | // }); 204 | // 205 | // let mut torrent = Torrent::new(disk_tx.clone(), daemon_tx, magnet); 206 | // torrent.size = info.get_size(); 207 | // 208 | // // pretend we already have the info, 209 | // // that was downloaded from the magnet 210 | // let mut torrent_info = torrent.ctx.info.write().await; 211 | // *torrent_info = info.clone(); 212 | // drop(torrent_info); 213 | // 214 | // let torrent_ctx = torrent.ctx.clone(); 215 | // 216 | // spawn(async move { 217 | // torrent.run().await.unwrap(); 218 | // }); 219 | // 220 | // let socket = TcpStream::connect(seeder).await.unwrap(); 221 | // let local = socket.local_addr().unwrap(); 222 | // let local_peer_id = Tracker::gen_peer_id(); 223 | // 224 | // let (socket, handshake) = 225 | // Peer::handshake(socket, Direction::Outbound, info_hash, 226 | // local_peer_id) .await 227 | // .unwrap(); 228 | // 229 | // // do not change the pieces here, 230 | // // this peer does not have anything downloaded 231 | // let mut peer = Peer::new(seeder, torrent_ctx, handshake, local); 232 | // let tx = peer.ctx.tx.clone(); 233 | // peer.session.state.peer_choking = false; 234 | // peer.session.state.am_interested = true; 235 | // peer.have_info = true; 236 | // 237 | // spawn(async move { 238 | // peer.run(Direction::Outbound, socket).await.unwrap(); 239 | // }); 240 | // 241 | // tx.send(PeerMsg::RequestBlockInfos(vec![BlockInfo { 242 | // index: 0, 243 | // begin: 0, 244 | // len: 15, 245 | // }])) 246 | // .await 247 | // .unwrap(); 248 | // tokio::time::sleep(Duration::from_secs(5)).await; 249 | // std::fs::remove_dir_all(download_dir).unwrap(); 250 | // } 251 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # This image is used to test github workflows locally 3 | # 4 | # Use the official Ubuntu base image 5 | FROM ubuntu:latest 6 | 7 | # Set the maintainer label 8 | LABEL maintainer="gabrielgcr45@gmail.com" 9 | 10 | # Install curl, build-essentials, and other dependencies 11 | RUN apt-get update && \ 12 | apt-get install -y \ 13 | curl \ 14 | build-essential \ 15 | git \ 16 | pkg-config \ 17 | libssl-dev \ 18 | sudo \ 19 | zip \ 20 | qemu \ 21 | qemu-user-static 22 | 23 | # Install Node.js 24 | RUN curl -fsSL https://deb.nodesource.com/setup_21.x | bash - && \ 25 | apt-get install -y nodejs 26 | 27 | # Install Rust using rustup 28 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 29 | 30 | # Add cargo to PATH 31 | ENV PATH="/root/.cargo/bin:${PATH}" 32 | 33 | # Install the GitHub CLI 34 | RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \ 35 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \ 36 | apt update && \ 37 | apt install gh -y 38 | 39 | # Install Docker 40 | RUN apt-get install -y apt-transport-https ca-certificates curl software-properties-common lsb-release && \ 41 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \ 42 | add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && \ 43 | apt-get update && \ 44 | apt-get install -y docker-ce docker-ce-cli containerd.io 45 | 46 | # Verify installations 47 | RUN rustc --version && \ 48 | cargo --version && \ 49 | docker --version && \ 50 | gh --version && \ 51 | node --version 52 | 53 | # Set the default command 54 | CMD [ "/bin/bash" ] 55 | 56 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1722813957, 24 | "narHash": "sha256-IAoYyYnED7P8zrBFMnmp7ydaJfwTnwcnqxUElC1I26Y=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "cb9a96f23c491c081b38eab96d22fa958043c9fa", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1718428119, 40 | "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1722997267, 66 | "narHash": "sha256-8Pncp8IKd0f0N711CRrCGTC4iLfBE+/5kaMqyWxnYic=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "d720bf3cebac38c2426d77ee2e59943012854cb8", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | rust-overlay.url = "github:oxalica/rust-overlay"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { 9 | nixpkgs, 10 | rust-overlay, 11 | flake-utils, 12 | ... 13 | }: 14 | flake-utils.lib.eachDefaultSystem ( 15 | system: let 16 | overlays = [(import rust-overlay)]; 17 | pkgs = import nixpkgs { 18 | inherit system overlays; 19 | }; 20 | in 21 | with pkgs; { 22 | devShells.default = mkShell { 23 | shellHook = '' 24 | export XDG_DOWNLOAD_DIR="$HOME/Downloads"; 25 | export XDG_CONFIG_HOME="$HOME/.config"; 26 | export XDG_STATE_HOME="$HOME/.local/state"; 27 | export XDG_DATA_HOME="$HOME/.local/share"; 28 | ''; 29 | buildInputs = [ 30 | taplo 31 | pkg-config 32 | glib 33 | ( 34 | rust-bin.selectLatestNightlyWith (toolchain: 35 | toolchain.default.override { 36 | extensions = ["rust-src"]; 37 | }) 38 | ) 39 | ]; 40 | }; 41 | } 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | comment_width = 80 3 | format_code_in_doc_comments = true 4 | imports_granularity = "Crate" 5 | # imports_layout = "Horizontal" 6 | wrap_comments = true 7 | use_small_heuristics = "Max" 8 | -------------------------------------------------------------------------------- /tape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/tape.gif -------------------------------------------------------------------------------- /tape.tape: -------------------------------------------------------------------------------- 1 | # This is a vhs script. See https://github.com/charmbracelet/vhs for more info. 2 | # To run this script, install vhs and run `vhs ./examples/block.tape` 3 | 4 | Set Theme "Catppuccin Macchiato" 5 | Output tape.gif 6 | Set Width 900 7 | Set Height 700 8 | Set Margin 15 9 | Set BorderRadius 20 10 | Set MarginFill "#6B50FF" 11 | 12 | Type "cargo run --release" 13 | Sleep 1s 14 | Enter 15 | Sleep 2s 16 | Type "t" 17 | Hide 18 | Type@0 "magnet:?xt=urn:btih:2C6B6858D61DA9543D4231A71DB4B1C9264B0685&dn=Ubuntu%2022.04%20LTS&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.bittor.pw%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fbt.xxx-tracker.com%3A2710%2Fannounce&tr=udp%3A%2F%2Fpublic.popcorn-tracker.org%3A6969%2Fannounce&tr=udp%3A%2F%2Feddie4.nl%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fp4p.arenabg.com%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce" 19 | Show 20 | Sleep 2s 21 | Enter 22 | Sleep 16s 23 | -------------------------------------------------------------------------------- /test-files/book.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/test-files/book.torrent -------------------------------------------------------------------------------- /test-files/debian.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/test-files/debian.torrent -------------------------------------------------------------------------------- /test-files/foo.txt: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /test-files/music.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/test-files/music.torrent -------------------------------------------------------------------------------- /test-files/pieces.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrieldemian/vincenzo/dbe81a7bab1945ae5f40e66d556384f9915b128a/test-files/pieces.iso --------------------------------------------------------------------------------