initial commit

This commit is contained in:
yggverse 2025-09-09 10:35:07 +03:00
parent 99c16e6d4c
commit 4a5b82bf39
7 changed files with 184 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
custom: https://yggverse.github.io/#donate

25
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Build
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup update
- run: cargo update
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets
- run: cargo build --verbose
- run: cargo test --verbose

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "btracker-scrape"
version = "0.1.0"
edition = "2024"
license = "MIT"
readme = "README.md"
description = "Shared BitTorrent scrape API for the βtracker project components"
keywords = ["btracker", "bittorrent", "scrape", "udp", "tcp"]
categories = ["network-programming"]
repository = "https://github.com/YGGverse/btracker-scrape"
[dependencies]
rand = "0.9.2"

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# btracker-scrape
![Build](https://github.com/YGGverse/btracker-scrape/actions/workflows/build.yml/badge.svg)
[![Dependencies](https://deps.rs/repo/github/YGGverse/btracker-scrape/status.svg)](https://deps.rs/repo/github/YGGverse/btracker-scrape)
[![crates.io](https://img.shields.io/crates/v/btracker-scrape.svg)](https://crates.io/crates/btracker-scrape)
Shared BitTorrent scrape API for the βtracker project components
## Install
``` bash
cargo add btracker-scrape
```

36
src/lib.rs Normal file
View file

@ -0,0 +1,36 @@
mod udp;
use std::net::SocketAddr;
use udp::Udp;
#[derive(Default)]
pub struct Result {
pub leechers: u32,
pub peers: u32,
pub seeders: u32,
}
pub struct Scrape {
udp: Option<Udp>,
// tcp: @TODO
}
impl Scrape {
pub fn init(udp: Option<(Vec<SocketAddr>, Vec<SocketAddr>)>) -> Self {
Self {
udp: udp.map(|(local, remote)| Udp::init(local, remote)),
}
}
pub fn scrape(&self, info_hash: [u8; 20]) -> Option<Result> {
self.udp.as_ref()?;
let mut t = Result::default();
if let Some(ref u) = self.udp {
let r = u.scrape(info_hash).ok()?; // @TODO handle
t.leechers += r.leechers;
t.peers += r.peers;
t.seeders += r.seeders;
}
Some(t)
}
}

94
src/udp.rs Normal file
View file

@ -0,0 +1,94 @@
use rand::Rng;
use std::{
io::Error,
net::{SocketAddr, UdpSocket},
time::Duration,
};
struct Route {
socket: UdpSocket,
remote: Vec<SocketAddr>,
}
pub struct Udp(Vec<Route>);
impl Udp {
pub fn init(local: Vec<SocketAddr>, remote: Vec<SocketAddr>) -> Self {
Self(
local
.into_iter()
.map(|l| {
let socket = UdpSocket::bind(l).unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
Route {
socket,
remote: if l.is_ipv4() {
remote.iter().filter(|r| r.is_ipv4()).cloned().collect()
} else {
remote.iter().filter(|r| r.is_ipv6()).cloned().collect()
},
}
})
.collect(),
)
}
pub fn scrape(&self, info_hash: [u8; 20]) -> Result<super::Result, Error> {
let mut t = super::Result::default();
for route in &self.0 {
for remote in &route.remote {
route.socket.send_to(&connection_request(), remote)?;
let mut b = [0u8; 16];
if route.socket.recv(&mut b)? < 16 {
todo!()
}
route.socket.send_to(
&scrape_request(
u64::from_be_bytes(b[8..16].try_into().unwrap()),
rand::rng().random::<u32>(),
&[info_hash],
),
remote,
)?;
let mut b = [0u8; 1024];
let l = route.socket.recv(&mut b)?;
if l < 20 {
todo!()
}
t.seeders += u32::from_be_bytes(b[8..12].try_into().unwrap());
t.leechers += u32::from_be_bytes(b[12..16].try_into().unwrap());
t.peers += u32::from_be_bytes(b[16..20].try_into().unwrap());
}
}
Ok(t)
}
}
fn connection_request() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&0x41727101980u64.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes());
b.extend_from_slice(&rand::rng().random::<u32>().to_be_bytes());
b
}
fn scrape_request(connection_id: u64, transaction_id: u32, info_hashes: &[[u8; 20]]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&connection_id.to_be_bytes());
b.extend_from_slice(&2u32.to_be_bytes());
b.extend_from_slice(&transaction_id.to_be_bytes());
// * up to about 74 torrents can be scraped at once
// https://www.bittorrent.org/beps/bep_0015.html
if info_hashes.len() > 74 {
todo!()
}
for hash in info_hashes {
b.extend_from_slice(hash);
}
b
}