Better error and tracker handling

This commit is contained in:
Arlo Filley 2023-10-06 16:40:43 +01:00
parent 5eb73b29ad
commit 930f50042d
11 changed files with 377 additions and 319 deletions

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/<executable file>",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -1,5 +0,0 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}

1
downloads/README Normal file
View File

@ -0,0 +1 @@
This is a test file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -17,107 +17,111 @@ mod message;
mod torrent;
mod tracker;
use core::panic;
use std::net::SocketAddr;
// Crate Imports
use crate::{
files::Files,
peer::Peer,
torrent::Torrent,
tracker::{
AnnounceMessage, AnnounceMessageResponse,
ConnectionMessage,
FromBuffer,
Tracker
}
files::Files,
peer::Peer,
torrent::Torrent,
tracker::tracker::Tracker
};
use tokio::sync::mpsc;
// External Ipmorts
use clap::Parser;
use log::{ debug, info, LevelFilter };
use log::{ debug, info, LevelFilter, error };
use tokio::spawn;
/// Struct Respresenting needed arguments
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
log_file_path: Option<String>,
#[arg(short, long)]
torrent_file_path: String,
#[arg(short, long)]
download_path: String,
#[arg(short, long)]
log_file_path: Option<String>,
#[arg(short, long)]
torrent_file_path: String,
#[arg(short, long)]
download_path: String,
#[arg(short, long)]
peer_id: String,
}
/// The root function
#[tokio::main]
async fn main() {
let args = Args::parse();
// Creates a log file to handle large amounts of data
let log_path = args.log_file_path.unwrap_or(String::from("./log/rustytorrent.log"));
simple_logging::log_to_file(&log_path, LevelFilter::Info).unwrap();
// Read the Torrent File
let torrent = Torrent::from_torrent_file(&args.torrent_file_path).await;
info!("Sucessfully read torrent file");
torrent.log_useful_information();
// Create the files that will be written to
let mut files = Files::new();
files.create_files(&torrent, &args.download_path).await;
// Gets peers from the given tracker
let (_remote_hostname, _remote_port) = torrent.get_tracker();
let (remote_hostname, remote_port) = ("tracker.opentrackr.org", 1337);
debug!("{}:{}", remote_hostname, remote_port);
let mut tracker = Tracker::new("0.0.0.0:61389", remote_hostname, remote_port).await;
info!("Successfully connected to tracker {}:{}", remote_hostname, remote_port);
let connection_message = ConnectionMessage::from_buffer(
&tracker.send_message(&ConnectionMessage::create_basic_connection()).await
);
debug!("{:?}", connection_message);
let announce_message_response = AnnounceMessageResponse::from_buffer(
&tracker.send_message(&AnnounceMessage::new(
connection_message.connection_id,
&torrent.get_info_hash(),
"-MY0001-123456654321",
torrent.get_total_length() as i64
)).await
);
debug!("{:?}", announce_message_response);
info!("Found Peers");
// Creates an assumed peer connection to the `SocketAddr` given
let mut peer = match Peer::create_connection(&format!("{}:{}", announce_message_response.ips[0], announce_message_response.ports[0])).await {
None => { return },
Some(peer) => peer
};
let num_pieces = torrent.info.pieces.len() / 20;
peer.handshake(&torrent).await;
peer.keep_alive_until_unchoke().await;
info!("Successfully Created Connection with peer: {}", peer.peer_id);
let mut len = 0;
for index in 0..num_pieces {
let piece= peer.request_piece(
index as u32, torrent.info.piece_length as u32,
&mut len, torrent.get_total_length() as u32
).await;
let args = Args::parse();
if torrent.check_piece(&piece, index as u32) {
files.write_piece(piece).await;
} else {
break
// Creates a log file to handle large amounts of data
let log_path = args.log_file_path.unwrap_or(String::from("./log/rustytorrent.log"));
//simple_logging::log_to_file(&log_path, LevelFilter::Debug).unwrap();
simple_logging::log_to_stderr(LevelFilter::Debug);
info!("==> WELCOME TO RUSTY-TORRENT <==");
// Read the Torrent File
let torrent = Torrent::from_torrent_file(&args.torrent_file_path).await;
torrent.log_useful_information();
// Create the files that will be written to
let mut files = Files::new();
files.create_files(&torrent, &args.download_path).await;
// Gets peers from the given tracker
let Some(socketaddrs) = torrent.get_trackers() else {
error!("couldn't find trackers");
panic!("couldn't find trackers")
};
let (remote_hostname, remote_port) = ("tracker.opentrackr.org", 1337);
debug!("{}:{}", remote_hostname, remote_port);
info!("");
info!("--> Finding Peers <--");
let listen_address = "0.0.0.0:61389".parse::<SocketAddr>().unwrap();
let Ok(mut tracker) = Tracker::new(listen_address, std::net::SocketAddr::V4(socketaddrs[0])).await else {
panic!("tracker couldn't be created")
};
info!("Successfully connected to tracker {}:{}", remote_hostname, remote_port);
let peers = tracker.find_peers(&torrent, &args.peer_id).await;
info!("Found Peers");
let num_pieces = torrent.info.pieces.len() / 20;
let mut peer = match Peer::create_connection(peers[0]).await {
None => { return },
Some(peer) => peer
};
peer.handshake(&torrent).await;
peer.keep_alive_until_unchoke().await;
info!("Successfully Created Connection with peer: {}", peer.peer_id);
println!("{}", peers.len());
let mut len = 0;
for index in 0..num_pieces {
let piece= peer.request_piece(
index as u32, torrent.info.piece_length as u32,
&mut len, torrent.get_total_length() as u32
).await;
if torrent.check_piece(&piece, index as u32) {
files.write_piece(piece).await;
} else {
break
}
}
}
peer.disconnect().await;
info!("Successfully completed download");
peer.disconnect().await;
info!("Successfully completed download");
}

View File

@ -49,7 +49,7 @@ impl FromBuffer for Message {
let message_length = u32::from_be_bytes(message_length);
let payload: Option<Vec<u8>>;
let mut payload: Option<Vec<u8>> = None;
let message_type: MessageType;
if message_length == 0 {
@ -66,9 +66,9 @@ impl FromBuffer for Message {
if end_of_message > buf.len() {
error!("index too long");
debug!("{buf:?}");
}
payload = Some(buf[5..end_of_message].to_vec());
} else {
payload = Some(buf[5..end_of_message].to_vec());
}
}
Self {

View File

@ -9,7 +9,7 @@ use crate::{
// External imports
use log::{ debug, error };
use std::net::SocketAddr;
use std::net::{SocketAddr, SocketAddrV4};
use tokio::{
io::{ AsyncReadExt, AsyncWriteExt },
net::TcpStream
@ -20,7 +20,7 @@ pub struct Peer {
/// The `TcpStream` that is used to communicate with the peeer
connection_stream: TcpStream,
/// The `SocketAddr` of the peer
pub socket_addr: SocketAddr,
pub socket_addr: SocketAddrV4,
/// The id of the peer
pub peer_id: String,
/// Whether the peer is choking the client
@ -33,29 +33,21 @@ impl Peer {
/// # Arguments
///
/// * `socket_address` - The socket address of the peer.
pub async fn create_connection(socket_address: &str) -> Option<Self> {
let socket_addr = match socket_address.parse::<SocketAddr>() {
Err(err) => {
error!("error parsing address {}, err: {}", socket_address, err);
return None;
}
Ok(addr) => { addr }
};
let connection_stream = match TcpStream::connect(socket_addr).await {
pub async fn create_connection(socket_address: SocketAddrV4) -> Option<Self> {
let connection_stream = match TcpStream::connect(socket_address).await {
Err(err) => {
error!("unable to connect to {}, err: {}", socket_address, err);
return None
},
Ok(stream) => {
debug!("created tcpstream successfully to: {socket_addr}");
debug!("created tcpstream successfully to: {socket_address}");
stream
}
};
Some(Self {
connection_stream,
socket_addr,
socket_addr: socket_address,
peer_id: String::new(),
choking: true,
})

View File

@ -1,8 +1,9 @@
use log::{debug, info, error};
use log::{debug, info, error, warn, trace};
use regex::Regex;
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use tokio::{fs::File as TokioFile, io::AsyncReadExt};
use std::net::{IpAddr, SocketAddrV4};
/// Represents a node in a DHT network.
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -11,203 +12,238 @@ struct Node(String, i64);
/// Represents a file described in a torrent.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct File {
pub path: Vec<String>,
pub length: u64,
#[serde(default)]
md5sum: Option<String>,
pub path: Vec<String>,
pub length: u64,
#[serde(default)]
md5sum: Option<String>,
}
/// Represents the metadata of a torrent.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Info {
pub name: String,
#[serde(with = "serde_bytes")]
pub pieces: Vec<u8>,
#[serde(rename = "piece length")]
pub piece_length: u64,
#[serde(default)]
md5sum: Option<String>,
#[serde(default)]
pub length: Option<i64>,
#[serde(default)]
pub files: Option<Vec<File>>,
#[serde(default)]
private: Option<u8>,
#[serde(default)]
path: Option<Vec<String>>,
#[serde(default)]
#[serde(rename = "root hash")]
root_hash: Option<String>,
pub name: String,
#[serde(with = "serde_bytes")]
pub pieces: Vec<u8>,
#[serde(rename = "piece length")]
pub piece_length: u64,
#[serde(default)]
md5sum: Option<String>,
#[serde(default)]
pub length: Option<i64>,
#[serde(default)]
pub files: Option<Vec<File>>,
#[serde(default)]
private: Option<u8>,
#[serde(default)]
path: Option<Vec<String>>,
#[serde(default)]
#[serde(rename = "root hash")]
root_hash: Option<String>,
}
/// Represents a torrent.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Torrent {
pub info: Info,
#[serde(default)]
pub announce: Option<String>,
#[serde(default)]
nodes: Option<Vec<Node>>,
#[serde(default)]
encoding: Option<String>,
#[serde(default)]
httpseeds: Option<Vec<String>>,
#[serde(default)]
#[serde(rename = "announce-list")]
announce_list: Option<Vec<Vec<String>>>,
#[serde(default)]
#[serde(rename = "creation date")]
creation_date: Option<i64>,
#[serde(rename = "comment")]
comment: Option<String>,
#[serde(default)]
#[serde(rename = "created by")]
created_by: Option<String>
pub info: Info,
#[serde(default)]
pub announce: Option<String>,
#[serde(default)]
nodes: Option<Vec<Node>>,
#[serde(default)]
encoding: Option<String>,
#[serde(default)]
httpseeds: Option<Vec<String>>,
#[serde(default)]
#[serde(rename = "announce-list")]
announce_list: Option<Vec<Vec<String>>>,
#[serde(default)]
#[serde(rename = "creation date")]
creation_date: Option<i64>,
#[serde(rename = "comment")]
comment: Option<String>,
#[serde(default)]
#[serde(rename = "created by")]
created_by: Option<String>
}
impl Torrent {
/// Reads a `.torrent` file and converts it into a `Torrent` struct.
///
/// # Arguments
///
/// * `path` - The path to the `.torrent` file.
pub async fn from_torrent_file(path: &str) -> Self {
// Read .torrent File
let mut file = TokioFile::open(path).await.unwrap();
let mut buf: Vec<u8> = Vec::new();
match file.read_to_end(&mut buf).await {
Err(err) => {
error!("Error reading file till end: {err}");
panic!("Error reading file till end: {err}");
}
Ok(_) => { info!("Succesfully read {path}") }
}
match serde_bencode::from_bytes(&buf) {
Err(err) => {
error!("Error deserializing file: {err}");
panic!("Error deserializing file: {err}");
}
Ok(torrent) => { torrent }
}
}
/// Logs info about the *.torrent file
pub fn log_useful_information(&self) {
info!(" -> Torrent Information <- ");
info!("Name: {}", self.info.name);
info!("Tracker: {:?}", self.announce);
info!("Tracker List: {:?}", self.announce_list);
info!("Info Hash: {:X?}", self.get_info_hash());
info!("Length: {:?}", self.info.length);
info!("Files:");
match &self.info.files {
None => {
info!("> {}", self.info.name);
}
Some(files) => {
for file in files {
let mut path = String::new();
for section in &file.path {
path.push_str(&format!("{section}/"));
}
path.pop();
info!("> {}: {}B", path, file.length)
}
}
}
}
/// Calculates the info hash of the torrent.
pub fn get_info_hash(&self) -> Vec<u8> {
let buf = serde_bencode::to_bytes(&self.info).unwrap();
let mut hasher = Sha1::new();
hasher.update(buf);
let res = hasher.finalize();
res[..].to_vec()
}
/// Checks if a downloaded piece matches its hash.
///
/// # Arguments
///
/// * `piece` - The downloaded piece.
/// * `index` - The index of the piece.
///
/// # Returns
///
/// * `true` if the piece is correct, `false` otherwise.
pub fn check_piece(&self, piece: &[u8], index: u32) -> bool {
let mut hasher = Sha1::new();
hasher.update(piece);
let result = hasher.finalize();
let piece_hash = &self.info.pieces[(index * 20) as usize..(index * 20 + 20) as usize];
if &result[..] == piece_hash {
info!("Downloaded Piece {}/{} Correct!, Piece Was: {}B long", index + 1, self.info.pieces.len() / 20, piece.len(),);
true
} else {
debug!("{:?}", &result[..]);
debug!("{:?}", piece_hash);
debug!("{:?}", &result[..].len());
debug!("{:?}", piece_hash.len());
debug!("{}", piece.len());
error!("Piece downloaded incorrectly");
false
}
}
pub fn get_total_length(&self) -> u64 {
match self.info.length {
None => {},
Some(n) => { return n as u64 }
};
match &self.info.files {
None => { 0 },
Some(files) => {
let mut n = 0;
/// Reads a `.torrent` file and converts it into a `Torrent` struct.
///
/// # Arguments
///
/// * `path` - The path to the `.torrent` file.
pub async fn from_torrent_file(path: &str) -> Self {
info!("");
info!("--> Reading File <--");
let Ok(mut file) = TokioFile::open(path).await else {
error!("Unable to read file at {path}");
panic!("Unable to read file at {path}");
};
info!("Found\t\t > {path}");
let mut buf: Vec<u8> = Vec::new();
let Ok(_) = file.read_to_end(&mut buf).await else {
error!("Error reading file > {path}");
panic!("Error reading file > {path}");
};
info!("Read\t\t > {path}");
let Ok(torrent) = serde_bencode::from_bytes(&buf) else {
error!("Error deserializing file > {path}");
panic!("Error deserializing file > {path}");
};
info!("Parsed\t > {path}");
torrent
}
}
impl Torrent {
/// Logs info about the *.torrent file
pub fn log_useful_information(&self) {
info!("");
info!("--> Torrent Information <--");
info!("Name:\t\t{}", self.info.name);
info!("Trackers");
if let Some(trackers) = &self.announce_list {
for tracker in trackers {
info!(" |> {}", tracker[0])
}
}
info!("InfoHash:\t{:X?}", self.get_info_hash());
info!("Length:\t{:?}", self.info.length);
info!("Files:");
let Some(mut files) = self.info.files.clone() else {
info!("./{}", self.info.name);
return
};
files.sort_by(|a, b| a.path.len().cmp(&b.path.len()) );
info!("./");
for file in files {
n += file.length;
if file.path.len() == 1 {
info!(" |--> {:?}", file.path);
} else {
let mut path = String::new();
file.path.iter().for_each(|s| { path.push_str(s); path.push('/') } );
path.pop();
info!(" |--> {}: {}B", path, file.length)
}
}
}
/// Calculates the info hash of the torrent.
pub fn get_info_hash(&self) -> Vec<u8> {
let buf = serde_bencode::to_bytes(&self.info).unwrap();
let mut hasher = Sha1::new();
hasher.update(buf);
let res = hasher.finalize();
res[..].to_vec()
}
/// Checks if a downloaded piece matches its hash.
///
/// # Arguments
///
/// * `piece` - The downloaded piece.
/// * `index` - The index of the piece.
///
/// # Returns
///
/// * `true` if the piece is correct, `false` otherwise.
pub fn check_piece(&self, piece: &[u8], index: u32) -> bool {
let mut hasher = Sha1::new();
hasher.update(piece);
let result = hasher.finalize();
let piece_hash = &self.info.pieces[(index * 20) as usize..(index * 20 + 20) as usize];
if &result[..] == piece_hash {
info!("Piece {}/{} Correct!", index + 1, self.info.pieces.len() / 20);
true
} else {
error!("Piece {}/{} incorrect :(", index + 1, self.info.pieces.len() / 20);
debug!("{:?}", &result[..]);
debug!("{:?}", piece_hash);
debug!("{:?}", &result[..].len());
debug!("{:?}", piece_hash.len());
debug!("{}", piece.len());
false
}
}
pub fn get_total_length(&self) -> u64 {
if let Some(n) = self.info.length {
return n as u64
};
n
}
if let Some(files) = &self.info.files {
let mut n = 0;
for file in files {
n += file.length;
};
return n
};
0
}
}
pub fn get_tracker(&self) -> (&str, u16) {
let re = Regex::new(r"^udp://([^:/]+):(\d+)/announce$").unwrap();
if let Some(url) = &self.announce {
if let Some(captures) = re.captures(url) {
let hostname = captures.get(1).unwrap().as_str();
let port = captures.get(2).unwrap().as_str();
pub fn get_trackers(&self) -> Option<Vec<SocketAddrV4>> {
info!("");
info!("--> Locating Trackers <--");
let mut addresses = vec![];
// This is the current regex as I haven't implemented support for http trackers yet
let re = Regex::new(r"^udp://([^:/]+):(\d+)/announce$").unwrap();
return (hostname, port.parse().unwrap());
} else {
println!("URL does not match the expected pattern | {}", url);
}
}
for (i, url) in self.announce_list.as_ref().unwrap().iter().enumerate() {
debug!("{:?}", url);
if let Some(captures) = re.captures(&url[i]) {
let hostname = captures.get(1).unwrap().as_str();
let port = captures.get(2).unwrap().as_str();
if let Some(url) = &self.announce {
if let Some(captures) = re.captures(url) {
let hostname = captures.get(1).unwrap().as_str();
let port = captures.get(2).unwrap().as_str();
if let Ok(ip) = dns_lookup::lookup_host(hostname) {
for i in ip {
if let IpAddr::V4(j) = i {
addresses.push(SocketAddrV4::new(j, port.parse().unwrap()))
}
}
}
} else {
warn!("{url} does not match the expected url pattern");
}
}
return (hostname, port.parse().unwrap());
} else {
println!("URL does not match the expected pattern | {}", url[i]);
}
if let Some(urls) = &self.announce_list {
for url in urls.iter() {
if let Some(captures) = re.captures(&url[0]) {
let hostname = captures.get(1).unwrap().as_str();
let port = captures.get(2).unwrap().as_str();
if let Ok(ip) = dns_lookup::lookup_host(hostname) {
for i in ip {
if let IpAddr::V4(j) = i {
addresses.push(SocketAddrV4::new(j, port.parse().unwrap()));
}
}
info!("Sucessfully found tracker {}", url[0]);
}
} else {
warn!("{} does not match the expected url pattern", url[0]);
}
}
}
if addresses.len() > 0 {
Some(addresses)
} else {
None
}
}
("", 0)
}
}

1
src/tracker/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod tracker;

View File

@ -1,15 +1,17 @@
use std::{net::{SocketAddr, Ipv4Addr}, vec};
use std::net::{SocketAddr, Ipv4Addr, SocketAddrV4};
use tokio::net::UdpSocket;
use log::{debug, error};
use crate::torrent::Torrent;
pub struct Tracker {
/// A UdpSocket used for communication.
connection_stream: UdpSocket,
/// The local socket address requests are made from
pub socket_addr: SocketAddr,
listen_address: SocketAddr,
/// The remote socket address of the tracker.
pub remote_addr: SocketAddr
remote_address: SocketAddr
}
impl Tracker {
@ -24,46 +26,30 @@ impl Tracker {
/// # Panics
///
/// Panics if there is an error parsing the given address or creating the UDP socket.
pub async fn new(socket_address: &str, remote_hostname: &str, remote_port: u16) -> Self {
let socket_addr = match socket_address.parse::<SocketAddr>() {
Err(err) => {
error!("error parsing address {}, err: {}", socket_address, err);
panic!("error parsing given address, {}", err);
}
Ok(addr) => { addr }
pub async fn new(listen_address: SocketAddr, remote_address: SocketAddr) -> Result<Self, ()> {
let Ok(connection_stream) = UdpSocket::bind(listen_address).await else {
error!("error binding to udpsocket {listen_address}");
return Err(())
};
debug!("bound udpsocket successfully to: {listen_address}");
let remote_address = dns_lookup::lookup_host(remote_hostname).unwrap()[0];
let remote_addr = SocketAddr::new(remote_address, remote_port);
let connection_stream = match UdpSocket::bind(socket_addr).await {
match connection_stream.connect(remote_address).await {
Err(err) => {
error!("unable to bind to {}, err: {}", socket_address, err);
panic!("error creating udpsocket, {}", err);
},
Ok(stream) => {
debug!("bound udpsocket successfully to: {socket_addr}");
stream
}
};
match connection_stream.connect(remote_addr).await {
Err(err) => {
error!("unable to connect to {}, err: {}", remote_addr, err);
error!("unable to connect to {}, err: {}", remote_address, err);
panic!("error creating udpsocket, {}", err);
},
Ok(()) => {
debug!("successfully connected to: {remote_addr}");
debug!("successfully connected to: {remote_address}");
}
};
Self {
Ok(Self {
connection_stream,
socket_addr,
remote_addr
}
listen_address,
remote_address
})
}
/// Sends a message to the tracker and receives a response asynchronously.
@ -75,7 +61,7 @@ impl Tracker {
/// # Returns
///
/// A byte vector containing the received response.
pub async fn send_message<T: ToBuffer>(&mut self, message: &T) -> Vec<u8> {
async fn send_message<T: ToBuffer>(&mut self, message: &T) -> Vec<u8> {
let mut buf: Vec<u8> = vec![ 0; 16_384 ];
self.connection_stream.send(&message.to_buffer()).await.unwrap();
@ -83,6 +69,33 @@ impl Tracker {
buf
}
async fn send_handshake(&mut self) -> i64 {
ConnectionMessage::from_buffer(
&self.send_message(&ConnectionMessage::create_basic_connection()).await
).connection_id
}
pub async fn find_peers(&mut self, torrent: &Torrent, peer_id: &str) -> Vec<SocketAddrV4> {
let id = self.send_handshake().await;
let message = AnnounceMessage::new(
id,
&torrent.get_info_hash(),
peer_id,
torrent.get_total_length() as i64
);
let announce_message_response = AnnounceMessageResponse::from_buffer(&self.send_message(&message).await);
let mut peer_addresses = vec![];
for i in 0..announce_message_response.ips.len() {
peer_addresses.push(SocketAddrV4::new(announce_message_response.ips[i], announce_message_response.ports[i]))
}
peer_addresses
}
}
/// A trait for converting a type into a byte buffer.