From a05027895d0345ee9b163204bf94538f35e89eba Mon Sep 17 00:00:00 2001 From: ArloFilley Date: Fri, 24 Nov 2023 13:25:32 +0000 Subject: [PATCH] created library --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 10 +- Cargo.toml | 31 +- lib_rusty_torrent/Cargo.toml | 18 + {src => lib_rusty_torrent/src}/files.rs | 6 - lib_rusty_torrent/src/lib.rs | 5 + {src => lib_rusty_torrent/src}/peer.rs | 186 +++--- lib_rusty_torrent/src/peer_wire_protocol.rs | 554 ++++++++++++++++++ lib_rusty_torrent/src/torrent.rs | 394 +++++++++++++ .../src}/tracker.rs | 15 +- lib_rusty_torrent/test.torrent | Bin 0 -> 17780 bytes rusty_torrenter/.gitignore | 8 + rusty_torrenter/Cargo.toml | 29 + LICENSE => rusty_torrenter/LICENSE | 0 readme.md => rusty_torrenter/README.md | 0 {src => rusty_torrenter/src}/main.rs | 46 +- src/handshake.rs | 105 ---- src/message.rs | 258 -------- src/torrent.rs | 249 -------- src/tracker/mod.rs | 1 - 20 files changed, 1121 insertions(+), 794 deletions(-) create mode 100644 .DS_Store create mode 100644 lib_rusty_torrent/Cargo.toml rename {src => lib_rusty_torrent/src}/files.rs (93%) create mode 100644 lib_rusty_torrent/src/lib.rs rename {src => lib_rusty_torrent/src}/peer.rs (53%) create mode 100644 lib_rusty_torrent/src/peer_wire_protocol.rs create mode 100644 lib_rusty_torrent/src/torrent.rs rename {src/tracker => lib_rusty_torrent/src}/tracker.rs (95%) create mode 100644 lib_rusty_torrent/test.torrent create mode 100644 rusty_torrenter/.gitignore create mode 100644 rusty_torrenter/Cargo.toml rename LICENSE => rusty_torrenter/LICENSE (100%) rename readme.md => rusty_torrenter/README.md (100%) rename {src => rusty_torrenter/src}/main.rs (75%) delete mode 100644 src/handshake.rs delete mode 100644 src/message.rs delete mode 100644 src/torrent.rs delete mode 100644 src/tracker/mod.rs diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aaf0dc6f5d783581ccbf1d693d441dd9b944cc92 GIT binary patch literal 6148 zcmeHKK~BRk5FD2Z3LGdmE=YbsC4LY}IdJX+fR?I4ghUD{61UuW4DaIvvtBDw9D3k_ zDzqD|$64<<$=I@E0B*W@xB$igMs&f!A&VbO+QoY|h>-)L3w@NBAV-NMo_g69*hK|o z?2d4SdsJ9+v_apvD{P z!mW6%P@wMi;LejCPPQ}2uM+nA@7S;Ns>sV?O%`V}_W5S2%BuHXpGnW1+@v3-nwbKo zfGMzJ3dkx)8yzcJX$qJEra-HJtPh?pm_=+9!_vVb907+R?N zF3Ijp0aM^#Dd2{)`E1HhihFDC<7BV(^gFtk"] -exclude = ["testing/", "process/", ".vscode/", ".DS_STORE"] -license = "MIT" -keywords = ["bittorrent", "torrent", "torrentclient"] -readme = "README.md" -repository = "https://github.com/arlofilley/rusty_torrent" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = { version = "4.4.0", features = ["derive"] } -dns-lookup = "2.0.2" -log = "0.4.20" -regex = "1.9.4" -reqwest = "0.11.20" -serde = { version = "1.0.183", features = ["derive"] } -serde_bencode = "0.2.3" -serde_bytes = "0.11.12" -sha1 = "0.10.5" -simple-logging = "2.0.2" -tokio = { version = "1.30.0", features = ["full"] } +[workspace.dependencies] +tokio = { version = "1.30.0", features = ["full"] } \ No newline at end of file diff --git a/lib_rusty_torrent/Cargo.toml b/lib_rusty_torrent/Cargo.toml new file mode 100644 index 0000000..b1002e4 --- /dev/null +++ b/lib_rusty_torrent/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lib_rusty_torrent" +version = "0.1.0" +edition = "2021" + +[lib] +name = "lib_rusty_torrent" +crate-type = ["lib"] + +[dependencies] +tokio = { workspace = true } +serde = { version = "1.0.183", features = ["derive"] } +serde_bencode = "0.2.3" +serde_bytes = "0.11.12" +sha1 = "0.10.5" +dns-lookup = "2.0.2" +regex = "1.9.4" +reqwest = "0.11.20" \ No newline at end of file diff --git a/src/files.rs b/lib_rusty_torrent/src/files.rs similarity index 93% rename from src/files.rs rename to lib_rusty_torrent/src/files.rs index 249fe4f..6c5dedd 100644 --- a/src/files.rs +++ b/lib_rusty_torrent/src/files.rs @@ -1,4 +1,3 @@ -use log::debug; use tokio::{ fs::try_exists as dir_exists, fs::create_dir as create_dir, @@ -56,7 +55,6 @@ impl Files { path.push_str(dir); if !dir_exists(&path).await.unwrap() { - debug!("Creating: {path}"); create_dir(&path).await.unwrap(); } } @@ -64,8 +62,6 @@ impl Files { path.push('/'); path.push_str(&t_file.path[t_file.path.len() - 1]); - - debug!("Creating: {path}"); let file = File::create(&path).await.unwrap(); let length = t_file.length; @@ -92,14 +88,12 @@ impl Files { if file.current_length + piece_len > file.length { let n = file.file.write(&piece[j..(file.length - file.current_length) as usize]).await.unwrap(); - debug!("Wrote {n}B > {}", file.name); j = (file.length - file.current_length) as usize; file.current_length += j as u64; piece_len -= j as u64; file.complete = true; } else { let n = file.file.write(&piece[j..]).await.unwrap(); - debug!("Wrote {n}B > {}", file.name); file.current_length += piece_len; return } diff --git a/lib_rusty_torrent/src/lib.rs b/lib_rusty_torrent/src/lib.rs new file mode 100644 index 0000000..d900f55 --- /dev/null +++ b/lib_rusty_torrent/src/lib.rs @@ -0,0 +1,5 @@ +pub mod torrent; +pub mod peer_wire_protocol; +pub mod peer; +pub mod files; +pub mod tracker; \ No newline at end of file diff --git a/src/peer.rs b/lib_rusty_torrent/src/peer.rs similarity index 53% rename from src/peer.rs rename to lib_rusty_torrent/src/peer.rs index 2ba10fd..46c192a 100644 --- a/src/peer.rs +++ b/lib_rusty_torrent/src/peer.rs @@ -2,18 +2,15 @@ // Crate Imports use crate::{ - handshake::Handshake, - message::{ FromBuffer, Message, MessageType, ToBuffer }, + peer_wire_protocol::{ Handshake, Message, MessageType }, torrent::Torrent }; // External imports -use log::{ debug, error, info }; -use std::{net::{SocketAddr, SocketAddrV4, Ipv4Addr}, sync::mpsc::Sender}; +use std::net::SocketAddrV4; use tokio::{ - io::{ AsyncReadExt, AsyncWriteExt, Ready }, - net::TcpStream, sync::{oneshot, broadcast}, spawn, - sync::mpsc + io::{ AsyncReadExt, AsyncWriteExt }, + net::TcpStream }; /// Structure to abstract interaction with a peer. @@ -21,11 +18,11 @@ pub struct Peer { /// The `TcpStream` that is used to communicate with the peeer connection_stream: TcpStream, /// The `SocketAddr` of the peer - socket_addr: SocketAddrV4, + pub socket_addr: SocketAddrV4, /// The id of the peer pub peer_id: String, /// Whether the peer is choking the client - choking: bool, + pub choking: bool, } impl Peer { @@ -34,19 +31,17 @@ impl Peer { /// # Arguments /// /// * `socket_address` - The socket address of the peer. - pub async fn create_connection(socket_address: SocketAddrV4) -> Option { + pub async fn create_connection(socket_address: SocketAddrV4) -> Result { let connection_stream = match TcpStream::connect(socket_address).await { Err(err) => { - error!("unable to connect to {}, err: {}", socket_address, err); - return None + return Err(format!("unable to connect to {}, err: {}", socket_address, err)) }, Ok(stream) => { - debug!("created tcpstream successfully to: {socket_address}"); stream } }; - Some(Self { + Ok(Self { connection_stream, socket_addr: socket_address, peer_id: String::new(), @@ -55,63 +50,16 @@ impl Peer { } } -#[derive(Clone, Debug)] -pub enum ControlMessage { - DownloadPiece(u32, u32, u32, u32), - DownloadedPiece(Vec) -} - impl Peer { - pub async fn test(address: SocketAddrV4, torrent: Torrent) -> (broadcast::Sender, broadcast::Receiver) { - let (sender, mut receiver) = broadcast::channel::(16); - - let sx1 = sender.clone(); - let rx1 = receiver.resubscribe(); - let t = torrent.clone(); - - spawn(async move { - let mut peer = match Peer::create_connection(address).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); - - loop { - if receiver.is_empty() { - continue - } else { - let Ok(m) = receiver.recv().await else { - continue; - }; - - println!("{m:#?}"); - - match m { - ControlMessage::DownloadPiece(a, b, mut c, d) => { - let buf = peer.request_piece(a, b, &mut c, d).await; - let _ = sender.send(ControlMessage::DownloadedPiece(buf)); - } - _ => () - } - } - } - }); - - (sx1, rx1) - } - /// Sends a handshake message to the peer, the first step in the peer wire messaging protocol. /// /// # Arguments /// /// * `torrent` - The `Torrent` instance associated with the peer. - async fn handshake(&mut self, torrent: &Torrent) { + pub async fn handshake(&mut self, torrent: &Torrent) -> Result<(), String>{ let mut buf = vec![0; 1024]; - let handshake_message = Handshake::new(&torrent.get_info_hash()).unwrap(); + let handshake_message = Handshake::new(&torrent.get_info_hash(), String::from("-RT0001-123456012345")).unwrap(); self.connection_stream.writable().await.unwrap(); self.connection_stream.write_all(&handshake_message.to_buffer()).await.unwrap(); @@ -120,10 +68,9 @@ impl Peer { let _ = self.connection_stream.read(&mut buf).await.unwrap(); let handshake = Handshake::from_buffer(&buf[..68].to_vec()).unwrap(); - handshake.log_useful_information(); for message_buf in Message::number_of_messages(&buf[68..]).0 { - let message = Message::from_buffer(&message_buf); + let message: Message = (&*message_buf).try_into()?; if message.message_type == MessageType::Unchoke { self.choking = false; @@ -131,22 +78,23 @@ impl Peer { } self.peer_id = handshake.peer_id; + + Ok(()) } /// Keeps the connection alive and sends interested messages until the peer unchokes - async fn keep_alive_until_unchoke(&mut self) { + pub async fn keep_alive_until_unchoke(&mut self) -> Result<(), String> { loop { - let message = self.read_message().await; + let message = self.read_message().await?; - debug!("{message:?}"); match message.message_type { MessageType::Unchoke => { self.choking = false; break } MessageType::KeepAlive => { - self.send_message_no_response(Message::new(0, MessageType::KeepAlive, None)).await; - self.send_message_no_response(Message::new(1, MessageType::Interested, None)).await; + self.send_message_no_response(Message::new(0, MessageType::KeepAlive, None)).await?; + self.send_message_no_response(Message::new(1, MessageType::Interested, None)).await?; } MessageType::Choke => { self.choking = true; @@ -154,59 +102,68 @@ impl Peer { _ => { continue } } } + + Ok(()) } /// Sends a message to the peer and waits for a response, which it returns - async fn send_message(&mut self, message: Message) -> Message { - let mut buf = vec![0; 16_397]; + pub async fn send_message(&mut self, message: Message) -> Result { + let mut response = vec![0; 16_397]; + + let message: Vec = message.try_into()?; self.connection_stream.writable().await.unwrap(); - self.connection_stream.write_all(&message.to_buffer()).await.unwrap(); + self.connection_stream.write_all(&message).await.unwrap(); self.connection_stream.readable().await.unwrap(); - let _ = self.connection_stream.read_exact(&mut buf).await.unwrap(); + let _ = self.connection_stream.read_exact(&mut response).await.unwrap(); - Message::from_buffer(&buf) + Ok((*response).try_into()?) } /// Sends a message to the peer and waits for a response, which it returns - async fn send_message_exact_size_response(&mut self, message: Message, size: usize) -> Message { - let mut buf = vec![0; size]; + pub async fn send_message_exact_size_response(&mut self, message: Message, size: usize) -> Result { + let mut response = vec![0; size]; + + let message: Vec = message.try_into()?; self.connection_stream.writable().await.unwrap(); - self.connection_stream.write_all(&message.to_buffer()).await.unwrap(); + self.connection_stream.write_all(&message).await.unwrap(); self.connection_stream.readable().await.unwrap(); - let _ = self.connection_stream.read_exact(&mut buf).await.unwrap(); + let _ = self.connection_stream.read_exact(&mut response).await.unwrap(); - Message::from_buffer(&buf) + Ok((*response).try_into()?) } /// Sends a message but doesn't wait for a response - async fn send_message_no_response(&mut self, message: Message) { + pub async fn send_message_no_response(&mut self, message: Message) -> Result<(), String> { + + let message: Vec = message.try_into()?; self.connection_stream.writable().await.unwrap(); - self.connection_stream.write_all(&message.to_buffer()).await.unwrap(); + self.connection_stream.write_all(&message).await.unwrap(); + + Ok(()) } /// reads a message from the peer - async fn read_message(&mut self) -> Message { - let mut buf = vec![0; 16_397]; + pub async fn read_message(&mut self) -> Result { + let mut response = vec![0; 16_397]; self.connection_stream.readable().await.unwrap(); - let _ = self.connection_stream.read(&mut buf).await.unwrap(); + let _ = self.connection_stream.read(&mut response).await.unwrap(); - Message::from_buffer(&buf) + Ok((*response).try_into()?) } /// Shutsdown the connection stream - async fn disconnect(&mut self) { + pub async fn disconnect(&mut self) -> Result<(), String>{ match self.connection_stream.shutdown().await { Err(err) => { - error!("Error disconnecting from {}: {}", self.socket_addr, err); - panic!("Error disconnecting from {}: {}", self.socket_addr, err); + return Err(format!("Error disconnecting from {}: {}", self.socket_addr, err)); }, Ok(_) => { - debug!("Successfully disconnected from {}", self.socket_addr) + Ok(()) } } } @@ -214,7 +171,7 @@ impl Peer { impl Peer { // Sends the requests and reads responses to put a piece together - pub async fn request_piece(&mut self, index: u32, piece_length: u32, len: &mut u32, total_len: u32) -> Vec { + pub async fn request_piece(&mut self, index: u32, piece_length: u32, len: &mut u32, total_len: u32) -> Result, String> { let mut buf = vec![]; // Sequentially requests piece from the peer for offset in (0..piece_length).step_by(16_384) { @@ -223,15 +180,14 @@ impl Peer { let response: Message; if *len + 16_384 >= total_len { - debug!("Final Request {}", total_len - *len); length = total_len - *len; response = self.send_message_exact_size_response( - Message::create_request(index, offset, length), + Message::create_piece_request(index, offset, length), length as usize + 13 - ).await; + ).await?; } else { - response = self.send_message(Message::create_request(index, offset, length)).await; + response = self.send_message(Message::create_piece_request(index, offset, length)).await?; }; match response.message_type { @@ -244,14 +200,46 @@ impl Peer { buf.push(byte) } }, - _ => { debug!("didn't recieve expected piece request | Recieved: {:?}", response.message_type); } + _ => { } }; if *len >= total_len - 1 { - return buf; + return Ok(buf); } } - buf + Ok(buf) } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::torrent::Torrent; + use std::net::Ipv4Addr; + + #[tokio::test] + async fn peer_create_connection() { + // Replace the IP and port with the actual values + let socket_address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6881); + + match Peer::create_connection(socket_address).await { + Ok(peer) => { + assert_eq!(peer.socket_addr, socket_address); + // Add more assertions if needed + } + Err(err) => panic!("Unexpected error: {}", err), + } + } + + #[tokio::test] + async fn peer_handshake() { + let socket_address = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 6881); + let mut peer = Peer::create_connection(socket_address.clone()).await.unwrap(); + let torrent = Torrent::from_torrent_file("test.torrent").await.unwrap(); + + assert!(peer.handshake(&torrent).await.is_ok()); + } + + // Add more tests for other methods in the Peer structure +} diff --git a/lib_rusty_torrent/src/peer_wire_protocol.rs b/lib_rusty_torrent/src/peer_wire_protocol.rs new file mode 100644 index 0000000..d26357f --- /dev/null +++ b/lib_rusty_torrent/src/peer_wire_protocol.rs @@ -0,0 +1,554 @@ +/// Represents the handshake message that will be sent to a client. +#[derive(Debug)] +pub struct Handshake { + /// The length of the protocol name, must be 19 for "BitTorrent protocol". + p_str_len: u8, + /// The protocol name, should always be "BitTorrent protocol". + p_str: String, + /// Reserved for extensions, currently unused. + reserved: [u8; 8], + /// The infohash for the torrent. + info_hash: Vec, + /// The identifier for the client. + pub peer_id: String, +} + +impl Handshake { + /// Creates a new handshake. + /// + /// # Arguments + /// + /// * `info_hash` - The infohash for the torrent. + /// + /// # Returns + /// + /// A new `Handshake` instance on success, or an empty `Result` indicating an error. + pub fn new(info_hash: &[u8], peer_id: String) -> Result { + if info_hash.len() != 20 { + return Err(String::from("Incorrect infohash length")); + } + + if peer_id.len() != 20 { + return Err(String::from("Incorrect Peer_Id Length")) + } + + Ok(Self { + p_str_len: 19, + p_str: String::from("BitTorrent protocol"), + reserved: [0; 8], + info_hash: info_hash.to_vec(), + peer_id: String::from("-MY0001-123456654321") + }) + } + + /// Converts the `Handshake` instance to a byte buffer for sending to a peer. + /// + /// # Returns + /// + /// A byte vector containing the serialized handshake. + pub fn to_buffer(&self) -> Vec { + let mut buf: Vec = vec![0; 68]; + + buf[0] = self.p_str_len; + buf[1..20].copy_from_slice(&self.p_str.as_bytes()[..19]); + buf[21..28].copy_from_slice(&self.reserved[..7]); + buf[28..48].copy_from_slice(&self.info_hash[..20]); + buf[48..68].copy_from_slice(&self.peer_id.as_bytes()[..20]); + + buf + } + + /// Converts a byte buffer to a `Handshake` instance. + /// + /// # Arguments + /// + /// * `buf` - A byte vector containing the serialized handshake. + /// + /// # Returns + /// + /// A new `Handshake` instance on success, or an empty `Result` indicating an error. + /// + /// # Errors + /// + /// Returns an error if the provided buffer is not long enough (at least 68 bytes). + pub fn from_buffer(buf: &Vec) -> Result { + // Verify that buffer is at least the correct size, if not error + if buf.len() < 68 { + return Err(String::from("buffer provided to handshake was too short")); + } + + let mut p_str = String::new(); + for byte in buf.iter().take(20).skip(1) { + p_str.push(*byte as char) + } + + let mut info_hash: Vec = vec![0; 20]; + info_hash[..20].copy_from_slice(&buf[28..48]); + + let mut peer_id = String::new(); + for byte in buf.iter().take(68).skip(48) { + peer_id.push(*byte as char) + } + + Ok(Self { + p_str_len: buf[0], + p_str, + reserved: [0; 8], + info_hash, + peer_id + }) + } +} + +/// Represents a message in the BitTorrent protocol. +#[derive(Clone, Debug, PartialEq)] +pub struct Message { + /// The length of the message, including the type and payload. + pub message_length: u32, + /// The type of message. + pub message_type: MessageType, + /// The payload of the message, if any. + pub payload: Option>, +} + +impl Message { + /// Creates a new message. + /// + /// # Arguments + /// + /// * `message_length` - The length of the message. + /// * `message_type` - The type of message. + /// * `payload` - The payload of the message, if any. + pub fn new(message_length: u32, message_type: MessageType, payload: Option>) -> Self { + Self { message_length, message_type, payload } + } +} + +impl TryFrom<&[u8]> for Message { + type Error = String; + /// Decodes a message from a given buffer. + /// + /// # Arguments + /// + /// * `buf` - The byte buffer containing the serialized message. + /// + /// # Returns + /// + /// A new `Message` instance on success, or an empty `Result` indicating an error. + fn try_from(value: &[u8]) -> Result { + let mut message_length: [u8; 4] = [0; 4]; + + if value.len() < 5 { + return Err(format!("Buffer not long enough to be a message: Length {}, should be at least 4 bytes", value.len())); + } + + message_length[..4].copy_from_slice(&value[..4]); + + let message_length = u32::from_be_bytes(message_length); + + let payload: Option>; + let message_type: MessageType; + + if message_length == 0 { + message_type = MessageType::KeepAlive; + payload = None; + } else if message_length == 5 { + message_type = value[4].try_into()?; + payload = None; + } else { + message_type = value[4].try_into()?; + + let end_of_message = 4 + message_length as usize; + + if end_of_message > value.len() { + return Err(format!("Invalid message length {} expected {}", value.len(), end_of_message)) + } else { + payload = Some(value[5..end_of_message].to_vec()); + } + } + + Ok(Self { + message_length, + message_type, + payload + }) + } +} + + +impl TryFrom for Vec { + type Error = String; + /// Converts the `Message` instance to a byte buffer for sending. + /// + /// # Returns + /// + /// A byte vector containing the serialized message. + fn try_from(value: Message) -> Result { + let mut buf: Vec = vec![]; + + for byte in value.message_length.to_be_bytes() { + buf.push(byte); + } + + match value.message_type { + MessageType::KeepAlive => { + return Ok(buf) + }, + MessageType::Choke | MessageType::Unchoke | MessageType::Interested | MessageType::NotInterested => { + buf.push(value.message_type.try_into()?); + return Ok(buf); + }, + MessageType::Have | MessageType::Bitfield | MessageType::Request | MessageType::Piece | MessageType::Cancel | MessageType::Port => { + buf.push(value.message_type.try_into()?); + }, + } + + match value.payload { + None => { + return Err(String::from("Error you are trying to create a message that needs a payload with no payload")) + } + Some(payload) => { + buf.extend(payload); + } + } + + Ok(buf) + } +} + +impl Message { + /// Create a request message from a given piece_index, offset, and length + /// + /// # Arguments + /// + /// * `piece_index` - The index of the piece in the torrent + /// * `offset` - The offset within the piece, because requests should be no more than 16KiB + /// * `length` - The length of the piece request, should be 16KiB + /// + /// # Returns + /// + /// A piece request message + pub fn create_piece_request(piece_index: u32, offset: u32, length: u32) -> Self { + let mut payload: Vec = vec![]; + + for byte in piece_index.to_be_bytes() { + payload.push(byte); + } + + for byte in offset.to_be_bytes() { + payload.push(byte) + } + + for byte in length.to_be_bytes() { + payload.push(byte) + } + + Self { + message_length: 13, + message_type: MessageType::Request, + payload: Some(payload) + } + } + + /// Returns the number of messages in the given buffer and their contents. + /// + /// # Arguments + /// + /// * `buf` - The byte buffer containing multiple serialized messages. + /// + /// # Returns + /// + /// A tuple containing a vector of message byte buffers and the number of messages. + pub fn number_of_messages(buf: &[u8]) -> (Vec>, u32) { + let mut message_num = 0; + let mut messages: Vec> = vec![]; + + // Find the length of message one + // put that into an array and increment counter by one + let mut i = 0; // points to the front + let mut j; // points to the back + + loop { + j = u32::from_be_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]) as usize + 4; + + messages.push(buf[i..i+j].to_vec()); + i += j; + message_num += 1; + + if buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 0 && buf[i + 3] == 0 { + break; + } + } + + (messages, message_num) + } +} + +/// An enum representing all possible message types in the BitTorrent peer wire protocol. +#[derive(Clone, Debug, PartialEq)] +#[repr(u8)] +pub enum MessageType { + /// Keepalive message, 0 length. + /// Potential Errors if trying to handle a keepalive message like another message. + /// Due to length being 0, should always be explicitly handled. + KeepAlive = u8::MAX, + /// Message telling the client not to send any requests until the peer has unchoked, 1 length. + Choke = 0, + /// Message telling the client that it can send requests, 1 length. + Unchoke = 1, + /// Message indicating that the peer is still interested in downloading, 1 length. + Interested = 2, + /// Message indicating that the peer is not interested in downloading, 1 length. + NotInterested = 3, + /// Message indicating that the peer has a given piece, fixed length. + Have = 4, + /// Message sent after a handshake, represents the pieces that the peer has. + Bitfield = 5, + /// Request a given part of a piece based on index, offset, and length, 13 length. + Request = 6, + /// A response to a request with the accompanying data, varying length. + Piece = 7, + /// Cancels a request, 13 length. + Cancel = 8, + /// Placeholder for unimplemented message type. + Port = 9, +} + +impl TryFrom for u8 { + type Error = String; + fn try_from(value: MessageType) -> Result { + match value { + MessageType::Choke => Ok(0), + MessageType::Unchoke => Ok(1), + MessageType::Interested => Ok(2), + MessageType::NotInterested => Ok(3), + MessageType::Have => Ok(4), + MessageType::Bitfield => Ok(5), + MessageType::Request => Ok(6), + MessageType::Piece => Ok(7), + MessageType::Cancel => Ok(8), + MessageType::Port => Ok(9), + _ => { + Err(format!("Invalid Message Type {:?}", value)) + } + } + } +} + +impl TryFrom for MessageType { + type Error = String; + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(MessageType::Choke), + 1 => Ok(MessageType::Unchoke), + 2 => Ok(MessageType::Interested), + 3 => Ok(MessageType::NotInterested), + 4 => Ok(MessageType::Have), + 5 => Ok(MessageType::Bitfield), + 6 => Ok(MessageType::Request), + 7 => Ok(MessageType::Piece), + 8 => Ok(MessageType::Cancel), + 9 => Ok(MessageType::Port), + _ => { + Err(format!("Invalid Message Type {}", value)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handshake_creation() { + let info_hash: [u8; 20] = [1; 20]; + let peer_id = String::from("-MY0001-123456654321"); + + match Handshake::new(&info_hash, peer_id.clone()) { + Ok(handshake) => { + assert_eq!(handshake.p_str_len, 19); + assert_eq!(handshake.p_str, "BitTorrent protocol"); + assert_eq!(handshake.reserved, [0; 8]); + assert_eq!(handshake.info_hash, info_hash.to_vec()); + assert_eq!(handshake.peer_id, peer_id); + } + Err(_) => panic!("Unexpected error creating handshake"), + } + } + + #[test] + fn handshake_creation_invalid_infohash() { + let invalid_info_hash: [u8; 19] = [1; 19]; + let peer_id = String::from("-MY0001-123456654321"); + + match Handshake::new(&invalid_info_hash, peer_id.clone()) { + Err(err) => assert_eq!(err, "Incorrect infohash length"), + Ok(_) => panic!("Expected an error creating handshake, but got Ok"), + } + } + + #[test] + fn handshake_creation_invalid_peer_id() { + let info_hash: [u8; 20] = [1; 20]; + let invalid_peer_id = String::from("-INVALID"); + + match Handshake::new(&info_hash, invalid_peer_id) { + Err(err) => assert_eq!(err, "Incorrect Peer_Id Length"), + Ok(_) => panic!("Expected an error creating handshake, but got Ok"), + } + } + + #[test] + fn handshake_to_buffer() { + let info_hash: [u8; 20] = [1; 20]; + let peer_id = String::from("-MY0001-123456654321"); + let handshake = Handshake::new(&info_hash, peer_id).unwrap(); + let buffer = handshake.to_buffer(); + + assert_eq!(buffer.len(), 68); + // Add more assertions based on the expected structure of the buffer if needed + } + + #[test] + fn handshake_from_buffer() { + let info_hash: [u8; 20] = [1; 20]; + let peer_id = String::from("-MY0001-123456654321"); + let original_handshake = Handshake::new(&info_hash, peer_id.clone()).unwrap(); + let buffer = original_handshake.to_buffer(); + + match Handshake::from_buffer(&buffer) { + Ok(handshake) => assert_eq!(handshake.peer_id, peer_id), + Err(err) => panic!("Unexpected error: {}", err), + } + } + + #[test] + fn handshake_from_buffer_invalid_size() { + let short_buffer: Vec = vec![0; 67]; // Invalid size + match Handshake::from_buffer(&short_buffer) { + Err(err) => assert_eq!(err, "buffer provided to handshake was too short"), + Ok(_) => panic!("Expected an error, but got Ok"), + } + } + + #[test] + fn u8_to_message_type() { + assert_eq!(TryInto::::try_into(0 as u8), Ok(MessageType::Choke)); + assert_eq!(TryInto::::try_into(1 as u8), Ok(MessageType::Unchoke)); + assert_eq!(TryInto::::try_into(2 as u8), Ok(MessageType::Interested)); + assert_eq!(TryInto::::try_into(3 as u8), Ok(MessageType::NotInterested)); + assert_eq!(TryInto::::try_into(4 as u8), Ok(MessageType::Have)); + assert_eq!(TryInto::::try_into(5 as u8), Ok(MessageType::Bitfield)); + assert_eq!(TryInto::::try_into(6 as u8), Ok(MessageType::Request)); + assert_eq!(TryInto::::try_into(7 as u8), Ok(MessageType::Piece)); + assert_eq!(TryInto::::try_into(8 as u8), Ok(MessageType::Cancel)); + assert_eq!(TryInto::::try_into(9 as u8), Ok(MessageType::Port)); + assert_eq!(TryInto::::try_into(10 as u8), Err(String::from("Invalid Message Type 10"))); + } + + #[test] + fn message_type_to_u8() { + assert_eq!(TryInto::::try_into(MessageType::Choke), Ok(0 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Unchoke), Ok(1 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Interested), Ok(2 as u8)); + assert_eq!(TryInto::::try_into(MessageType::NotInterested), Ok(3 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Have), Ok(4 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Bitfield), Ok(5 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Request), Ok(6 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Piece), Ok(7 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Cancel), Ok(8 as u8)); + assert_eq!(TryInto::::try_into(MessageType::Port), Ok(9 as u8)); + assert_eq!(TryInto::::try_into(MessageType::KeepAlive), Err(String::from("Invalid Message Type KeepAlive"))); + } + + #[test] + fn create_piece_request() { + let piece_index = 42; + let offset = 1024; + let length = 16384; + + let request_message = Message::create_piece_request(piece_index, offset, length); + + assert_eq!(request_message.message_length, 13); + assert_eq!(request_message.message_type, MessageType::Request); + + if let Some(payload) = request_message.payload { + assert_eq!(payload.len(), 12); // 4 bytes for piece_index + 4 bytes for offset + 4 bytes for length + + let mut expected_payload = vec![]; + expected_payload.extend_from_slice(&piece_index.to_be_bytes()); + expected_payload.extend_from_slice(&offset.to_be_bytes()); + expected_payload.extend_from_slice(&length.to_be_bytes()); + + assert_eq!(payload, expected_payload); + } else { + panic!("Expected payload, but found None"); + } + } + + #[test] + fn try_from_valid_message() { + let message_bytes = vec![0, 0, 0, 5, 1]; // Unchoke message + + match Message::try_from(&message_bytes[..]) { + Ok(message) => { + assert_eq!(message.message_length, 5); + assert_eq!(message.message_type, MessageType::Unchoke); + assert!(message.payload.is_none()); + } + Err(err) => panic!("Unexpected error: {}", err), + } + } + + #[test] + fn try_from_invalid_message() { + let invalid_message_bytes = vec![0, 0, 0, 2]; // Message length indicates 2 bytes, but no payload provided + + match Message::try_from(&invalid_message_bytes[..]) { + Ok(_) => panic!("Expected an error but got Ok"), + Err(err) => { + assert_eq!( + err, + "Buffer not long enough to be a message: Length 4, should be at least 4 bytes" + ); + } + } + } + + #[test] + fn try_into_valid_message() { + let message = Message { + message_length: 5, + message_type: MessageType::Unchoke, + payload: None, + }; + + match Vec::::try_from(message) { + Ok(serialized_message) => { + assert_eq!(serialized_message, vec![0, 0, 0, 5, 1]); // Unchoke message + } + Err(err) => panic!("Unexpected error: {}", err), + } + } + + #[test] + fn try_into_message_with_payload() { + let payload_data = vec![65, 66, 67]; // Arbitrary payload + let message = Message { + message_length: 7, + message_type: MessageType::Piece, + payload: Some(payload_data.clone()), + }; + + match Vec::::try_from(message) { + Ok(serialized_message) => { + let mut expected_serialized_message = vec![0, 0, 0, 7, 7]; // Piece message + expected_serialized_message.extend_from_slice(&payload_data); + + assert_eq!(serialized_message, expected_serialized_message); + } + Err(err) => panic!("Unexpected error: {}", err), + } + } +} \ No newline at end of file diff --git a/lib_rusty_torrent/src/torrent.rs b/lib_rusty_torrent/src/torrent.rs new file mode 100644 index 0000000..ff77d31 --- /dev/null +++ b/lib_rusty_torrent/src/torrent.rs @@ -0,0 +1,394 @@ +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)] +struct Node(String, i64); + +/// Represents a file described in a torrent. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct File { + pub path: Vec, + pub length: u64, + #[serde(default)] + md5sum: Option, +} + +/// Represents the metadata of a torrent. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Info { + pub name: String, + #[serde(with = "serde_bytes")] + pub pieces: Vec, + #[serde(rename = "piece length")] + pub piece_length: u64, + #[serde(default)] + md5sum: Option, + #[serde(default)] + pub length: Option, + #[serde(default)] + pub files: Option>, + #[serde(default)] + private: Option, + #[serde(default)] + path: Option>, + #[serde(default)] + #[serde(rename = "root hash")] + root_hash: Option, +} + +/// Represents a torrent. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Torrent { + pub info: Info, + #[serde(default)] + pub announce: Option, + #[serde(default)] + nodes: Option>, + #[serde(default)] + encoding: Option, + #[serde(default)] + httpseeds: Option>, + #[serde(default)] + #[serde(rename = "announce-list")] + announce_list: Option>>, + #[serde(default)] + #[serde(rename = "creation date")] + creation_date: Option, + #[serde(rename = "comment")] + comment: Option, + #[serde(default)] + #[serde(rename = "created by")] + created_by: Option +} + +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) -> Result { + let Ok(mut file) = TokioFile::open(path).await else { + return Err(format!("Unable to read file at {path}")); + }; + + let mut buf: Vec = Vec::new(); + let Ok(_) = file.read_to_end(&mut buf).await else { + return Err(format!("Error reading file > {path}")); + }; + + let torrent: Torrent = match serde_bencode::from_bytes(&buf) { + Err(_) => return Err(format!("Error deserializing file > {path}")), + Ok(torrent) => torrent, + }; + + Ok(torrent) + } +} + +impl Torrent { + /// Calculates the info hash of the torrent. + pub fn get_info_hash(&self) -> Vec { + 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 { + true + } else { + false + } + } + + pub fn get_total_length(&self) -> u64 { + if let Some(n) = self.info.length { + return n as u64 + }; + + if let Some(files) = &self.info.files { + let mut n = 0; + + for file in files { + n += file.length; + }; + + return n + }; + + 0 + } + + pub fn get_trackers(&self) -> Result, String> { + 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(); + + 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())) + } + } + } + } + } + + 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())); + } + } + } + } + } + } + + if addresses.len() > 0 { + Ok(addresses) + } else { + Err(String::from("Unable to find trackers")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::runtime::Runtime; + + #[test] + fn from_torrent_file_success() { + let runtime = Runtime::new().unwrap(); + let path = "test.torrent"; + + let result = runtime.block_on(Torrent::from_torrent_file(path)); + println!("{result:?}"); + + assert!(result.is_ok()); + } + + #[test] + fn from_torrent_file_failure() { + let runtime = Runtime::new().unwrap(); + let path = "nonexistent/file.torrent"; + + let result = runtime.block_on(Torrent::from_torrent_file(path)); + + assert!(result.is_err()); + } + + #[test] + fn get_info_hash() { + // Create a mock Torrent instance + let torrent = Torrent { + info: Info { + name: String::from("test_torrent"), + pieces: vec![], + piece_length: 1024, + length: Some(2048), + files: None, + md5sum: None, + private: None, + path: None, + root_hash: None, + }, + announce: Some(String::from("http://tracker.example.com/announce")), + nodes: None, + encoding: None, + httpseeds: None, + announce_list: None, + creation_date: None, + comment: None, + created_by: None, + }; + + let result = torrent.get_info_hash(); + + assert!(!result.is_empty()); + } + + #[test] + fn check_piece_valid() { + let mut hasher = Sha1::new(); + hasher.update(vec![0; 1024]); + let piece_hash: &[u8] = &hasher.finalize(); + + // Create a mock Torrent instance + let torrent = Torrent { + info: Info { + name: String::from("test_torrent"), + pieces: piece_hash.into(), // Mock piece hashes + piece_length: 1024, + length: Some(2048), + files: None, + md5sum: None, + private: None, + path: None, + root_hash: None, + }, + announce: Some(String::from("http://tracker.example.com/announce")), + nodes: None, + encoding: None, + httpseeds: None, + announce_list: None, + creation_date: None, + comment: None, + created_by: None, + }; + + // Mock a valid piece + let piece = vec![0; 1024]; + + let result = torrent.check_piece(&piece, 0); + + assert!(result); + } + + #[test] + fn check_piece_invalid() { + // Create a mock Torrent instance + let torrent = Torrent { + info: Info { + name: String::from("test_torrent"), + pieces: vec![0; 20], // Mock piece hashes + piece_length: 1024, + length: Some(2048), + files: None, + md5sum: None, + private: None, + path: None, + root_hash: None, + }, + announce: Some(String::from("http://tracker.example.com/announce")), + nodes: None, + encoding: None, + httpseeds: None, + announce_list: None, + creation_date: None, + comment: None, + created_by: None, + }; + + // Mock an invalid piece + let piece = vec![1; 1024]; + + let result = torrent.check_piece(&piece, 0); + + assert!(!result); + } + + #[test] + fn get_total_length_single_file() { + // Create a mock Torrent instance with a single file + let torrent = Torrent { + info: Info { + name: String::from("test_torrent"), + pieces: vec![], + piece_length: 1024, + length: Some(2048), + files: Some(vec![File { + path: vec![String::from("test_file.txt")], + length: 2048, + md5sum: None, + }]), + md5sum: None, + private: None, + path: None, + root_hash: None, + }, + announce: Some(String::from("http://tracker.example.com/announce")), + nodes: None, + encoding: None, + httpseeds: None, + announce_list: None, + creation_date: None, + comment: None, + created_by: None, + }; + + let result = torrent.get_total_length(); + + assert_eq!(result, 2048); + } + + #[test] + fn get_total_length_multiple_files() { + // Create a mock Torrent instance with multiple files + let torrent = Torrent { + info: Info { + name: String::from("test_torrent"), + pieces: vec![], + piece_length: 1024, + length: None, + files: Some(vec![ + File { + path: vec![String::from("file1.txt")], + length: 1024, + md5sum: None, + }, + File { + path: vec![String::from("file2.txt")], + length: 2048, + md5sum: None, + }, + ]), + md5sum: None, + private: None, + path: None, + root_hash: None, + }, + announce: Some(String::from("http://tracker.example.com/announce")), + nodes: None, + encoding: None, + httpseeds: None, + announce_list: None, + creation_date: None, + comment: None, + created_by: None, + }; + + let result = torrent.get_total_length(); + + assert_eq!(result, 3072); + } + + // Add more tests for other methods and edge cases as needed +} \ No newline at end of file diff --git a/src/tracker/tracker.rs b/lib_rusty_torrent/src/tracker.rs similarity index 95% rename from src/tracker/tracker.rs rename to lib_rusty_torrent/src/tracker.rs index 90e7c0c..3699eec 100644 --- a/src/tracker/tracker.rs +++ b/lib_rusty_torrent/src/tracker.rs @@ -1,7 +1,6 @@ use std::net::{SocketAddr, Ipv4Addr, SocketAddrV4}; use tokio::net::UdpSocket; -use log::{debug, error}; use crate::torrent::Torrent; @@ -26,22 +25,16 @@ impl Tracker { /// # Panics /// /// Panics if there is an error parsing the given address or creating the UDP socket. - pub async fn new(listen_address: SocketAddr, remote_address: SocketAddr) -> Result { + pub async fn new(listen_address: SocketAddr, remote_address: SocketAddr) -> Result { let Ok(connection_stream) = UdpSocket::bind(listen_address).await else { - error!("error binding to udpsocket {listen_address}"); - return Err(()) + return Err(format!("error binding to udpsocket {listen_address}")) }; - - debug!("bound udpsocket successfully to: {listen_address}"); match connection_stream.connect(remote_address).await { Err(err) => { - error!("unable to connect to {}, err: {}", remote_address, err); - panic!("error creating udpsocket, {}", err); + return Err(format!("error creating udpsocket, {}", err)); }, - Ok(()) => { - debug!("successfully connected to: {remote_address}"); - } + Ok(()) => { } }; diff --git a/lib_rusty_torrent/test.torrent b/lib_rusty_torrent/test.torrent new file mode 100644 index 0000000000000000000000000000000000000000..cd1df575e8e02aef172740ceaca19a15ffa65383 GIT binary patch literal 17780 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 bWny%1a&l#EbS`OcFLZBma%FCGb1!9ORmHxU literal 0 HcmV?d00001 diff --git a/rusty_torrenter/.gitignore b/rusty_torrenter/.gitignore new file mode 100644 index 0000000..317483d --- /dev/null +++ b/rusty_torrenter/.gitignore @@ -0,0 +1,8 @@ +/target +Cargo.lock +/log/rusty_torrent.log +/testing +/process +/log +.DS_Store +/downloads \ No newline at end of file diff --git a/rusty_torrenter/Cargo.toml b/rusty_torrenter/Cargo.toml new file mode 100644 index 0000000..3edba30 --- /dev/null +++ b/rusty_torrenter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rusty_torrenter" +version = "0.9.3" +edition = "2021" + +description = "A BitTorrent client implemented in Rust that allows you to interact with the BitTorrent protocol and download torrents." + +authors = ["Arlo Filley "] +exclude = ["testing/", "process/", ".vscode/", ".DS_STORE"] +license = "MIT" +keywords = ["bittorrent", "torrent", "torrentclient"] +readme = "README.md" +repository = "https://github.com/arlofilley/rusty_torrent" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lib_rusty_torrent = { path = "../lib_rusty_torrent" } +dns-lookup = "2.0.2" +log = "0.4.20" +regex = "1.9.4" +reqwest = "0.11.20" +serde = { version = "1.0.183", features = ["derive"] } +serde_bencode = "0.2.3" +serde_bytes = "0.11.12" +sha1 = "0.10.5" +simple-logging = "2.0.2" +tokio = { workspace = true } +clap = { version = "*", features = ["derive"] } diff --git a/LICENSE b/rusty_torrenter/LICENSE similarity index 100% rename from LICENSE rename to rusty_torrenter/LICENSE diff --git a/readme.md b/rusty_torrenter/README.md similarity index 100% rename from readme.md rename to rusty_torrenter/README.md diff --git a/src/main.rs b/rusty_torrenter/src/main.rs similarity index 75% rename from src/main.rs rename to rusty_torrenter/src/main.rs index b3afe23..c027230 100644 --- a/src/main.rs +++ b/rusty_torrenter/src/main.rs @@ -9,30 +9,20 @@ //! Checks piece hashes //! Writes to torrent file -// Modules -mod files; -mod handshake; -mod peer; -mod message; -mod torrent; -mod tracker; - use core::panic; use std::net::SocketAddr; // Crate Imports -use crate::{ +use lib_rusty_torrent::{ files::Files, - peer::Peer, + peer::*, torrent::Torrent, - tracker::tracker::Tracker + tracker::Tracker, }; -use tokio::sync::mpsc; // External Ipmorts use clap::Parser; use log::{ debug, info, LevelFilter, error }; -use tokio::spawn; /// Struct Respresenting needed arguments #[derive(Parser, Debug)] @@ -64,8 +54,7 @@ async fn main() { info!("==> WELCOME TO RUSTY-TORRENT <=="); // Read the Torrent File - let torrent = Torrent::from_torrent_file(&args.torrent_file_path).await; - torrent.log_useful_information(); + let torrent = Torrent::from_torrent_file(&args.torrent_file_path).await.unwrap(); // Create the files that will be written to let mut files = Files::new(); @@ -102,28 +91,25 @@ async fn main() { let mut i = 0; let t = torrent.clone(); - let (sender, mut reciever) = Peer::test(peers[0], torrent).await; - loop { - let _ = sender.send(peer::ControlMessage::DownloadPiece(i, t.info.piece_length as u32, len, t.get_total_length() as u32)); - reciever.resubscribe(); - let a = reciever.recv().await.unwrap(); + let _ = sender.send(peer::ControlMessage::DownloadPiece(i, t.info.piece_length as u32, len, t.get_total_length() as u32)); + + let a = reciever.recv().await.unwrap(); - println!("2 {a:?}"); + println!("2 {a:?}"); - let peer::ControlMessage::DownloadedPiece(b) = a else { - continue; - }; + let peer::ControlMessage::DownloadedPiece(b) = a else { + continue; + }; - if t.check_piece(&b, i) { - files.write_piece(b).await; - } else { - break - } + if t.check_piece(&b, i) { + files.write_piece(b).await; + } else { + break } - //peer.disconnect().await; + peer.disconnect().await; info!("Successfully completed download"); diff --git a/src/handshake.rs b/src/handshake.rs deleted file mode 100644 index ab62671..0000000 --- a/src/handshake.rs +++ /dev/null @@ -1,105 +0,0 @@ -use log::{ info, error }; - -/// Represents the handshake message that will be sent to a client. -#[derive(Debug)] -pub struct Handshake { - /// The length of the protocol name, must be 19 for "BitTorrent protocol". - p_str_len: u8, - /// The protocol name, should always be "BitTorrent protocol". - p_str: String, - /// Reserved for extensions, currently unused. - reserved: [u8; 8], - /// The infohash for the torrent. - info_hash: Vec, - /// The identifier for the client. - pub peer_id: String, -} - -impl Handshake { - /// Creates a new handshake. - /// - /// # Arguments - /// - /// * `info_hash` - The infohash for the torrent. - /// - /// # Returns - /// - /// A new `Handshake` instance on success, or an empty `Result` indicating an error. - pub fn new(info_hash: &[u8]) -> Option { - if info_hash.len() != 20 { - error!("Incorrect infohash length, consider using the helper function in torrent"); - return None; - } - - Some(Self { - p_str_len: 19, - p_str: String::from("BitTorrent protocol"), - reserved: [0; 8], - info_hash: info_hash.to_vec(), - peer_id: String::from("-MY0001-123456654322") - }) - } - - /// Converts the `Handshake` instance to a byte buffer for sending to a peer. - /// - /// # Returns - /// - /// A byte vector containing the serialized handshake. - pub fn to_buffer(&self) -> Vec { - let mut buf: Vec = vec![0; 68]; - - buf[0] = self.p_str_len; - buf[1..20].copy_from_slice(&self.p_str.as_bytes()[..19]); - buf[21..28].copy_from_slice(&self.reserved[..7]); - buf[28..48].copy_from_slice(&self.info_hash[..20]); - buf[48..68].copy_from_slice(&self.peer_id.as_bytes()[..20]); - - buf - } - - /// Converts a byte buffer to a `Handshake` instance. - /// - /// # Arguments - /// - /// * `buf` - A byte vector containing the serialized handshake. - /// - /// # Returns - /// - /// A new `Handshake` instance on success, or an empty `Result` indicating an error. - /// - /// # Errors - /// - /// Returns an error if the provided buffer is not long enough (at least 68 bytes). - pub fn from_buffer(buf: &Vec) -> Option { - // Verify that buffer is at least the correct size, if not error - if buf.len() < 68 { - error!("buffer provided to handshake was too short"); - return None; - } - - let mut p_str = String::new(); - for byte in buf.iter().take(20).skip(1) { - p_str.push(*byte as char) - } - - let mut info_hash: Vec = vec![0; 20]; - info_hash[..20].copy_from_slice(&buf[28..48]); - - let mut peer_id = String::new(); - for byte in buf.iter().take(68).skip(48) { - peer_id.push(*byte as char) - } - - Some(Self { - p_str_len: buf[0], - p_str, - reserved: [0; 8], - info_hash, - peer_id - }) - } - - pub fn log_useful_information(&self) { - info!("Connected - PeerId: {:?}", self.peer_id); - } -} \ No newline at end of file diff --git a/src/message.rs b/src/message.rs deleted file mode 100644 index a36b5d1..0000000 --- a/src/message.rs +++ /dev/null @@ -1,258 +0,0 @@ -use std::vec; -use log::{error, debug}; - -/// Represents a message in the BitTorrent protocol. -#[derive(Debug, PartialEq)] -pub struct Message { - /// The length of the message, including the type and payload. - pub message_length: u32, - /// The type of message. - pub message_type: MessageType, - /// The payload of the message, if any. - pub payload: Option>, -} - -pub trait ToBuffer { - fn to_buffer(self) -> Vec; -} - -pub trait FromBuffer { - fn from_buffer(buf: &[u8]) -> Self; -} - -impl Message { - /// Creates a new message. - /// - /// # Arguments - /// - /// * `message_length` - The length of the message. - /// * `message_type` - The type of message. - /// * `payload` - The payload of the message, if any. - pub fn new(message_length: u32, message_type: MessageType, payload: Option>) -> Self { - Self { message_length, message_type, payload } - } -} - -impl FromBuffer for Message { - /// Decodes a message from a given buffer. - /// - /// # Arguments - /// - /// * `buf` - The byte buffer containing the serialized message. - /// - /// # Returns - /// - /// A new `Message` instance on success, or an empty `Result` indicating an error. - fn from_buffer(buf: &[u8]) -> Self { - let mut message_length: [u8; 4] = [0; 4]; - message_length[..4].copy_from_slice(&buf[..4]); - - let message_length = u32::from_be_bytes(message_length); - - let mut payload: Option> = None; - let message_type: MessageType; - - if message_length == 0 { - message_type = MessageType::KeepAlive; - payload = None; - } else if message_length == 5 { - message_type = buf[4].into(); - payload = None; - } else { - message_type = buf[4].into(); - - let end_of_message = 4 + message_length as usize; - - if end_of_message > buf.len() { - error!("index too long"); - debug!("{buf:?}"); - } else { - payload = Some(buf[5..end_of_message].to_vec()); - } - } - - Self { - message_length, - message_type, - payload - } - } -} - - -impl ToBuffer for Message { - /// Converts the `Message` instance to a byte buffer for sending. - /// - /// # Returns - /// - /// A byte vector containing the serialized message. - fn to_buffer(self) -> Vec { - let mut buf: Vec = vec![]; - - for byte in self.message_length.to_be_bytes() { - buf.push(byte); - } - - match self.message_type { - MessageType::KeepAlive => { - return buf - }, - MessageType::Choke | MessageType::Unchoke | MessageType::Interested | MessageType::NotInterested => { - buf.push(self.message_type.into()); - return buf; - }, - MessageType::Have | MessageType::Bitfield | MessageType::Request | MessageType::Piece | MessageType::Cancel | MessageType::Port => { - buf.push(self.message_type.into()); - }, - MessageType::Error => { - panic!("Error making message into buffer") - } - } - - match self.payload { - None => { panic!("Error you are trying to create a message that needs a payload with no payload") } - Some(payload) => { - buf.extend(payload); - } - } - - buf - } -} - -impl Message { - /// Create a request message from a given piece_index, offset, and length - /// - /// # Arguments - /// - /// * `piece_index` - The index of the piece in the torrent - /// * `offset` - The offset within the piece, because requests should be no more than 16KiB - /// * `length` - The length of the piece request, should be 16KiB - /// - /// # Returns - /// - /// A piece request message - pub fn create_request(piece_index: u32, offset: u32, length: u32) -> Self { - let mut payload: Vec = vec![]; - - for byte in piece_index.to_be_bytes() { - payload.push(byte); - } - - for byte in offset.to_be_bytes() { - payload.push(byte) - } - - for byte in length.to_be_bytes() { - payload.push(byte) - } - - Self { message_length: 13, message_type: MessageType::Request, payload: Some(payload) } - } - - /// Returns the number of messages in the given buffer and their contents. - /// - /// # Arguments - /// - /// * `buf` - The byte buffer containing multiple serialized messages. - /// - /// # Returns - /// - /// A tuple containing a vector of message byte buffers and the number of messages. - pub fn number_of_messages(buf: &[u8]) -> (Vec>, u32) { - let mut message_num = 0; - let mut messages: Vec> = vec![]; - - // Find the length of message one - // put that into an array and increment counter by one - let mut i = 0; // points to the front - let mut j; // points to the back - - loop { - j = u32::from_be_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]) as usize + 4; - - messages.push(buf[i..i+j].to_vec()); - i += j; - message_num += 1; - - if buf[i] == 0 && buf[i + 1] == 0 && buf[i + 2] == 0 && buf[i + 3] == 0 { - break; - } - } - - (messages, message_num) - } -} - -/// An enum representing all possible message types in the BitTorrent peer wire protocol. -#[derive(Clone, Debug, PartialEq)] -#[repr(u8)] -pub enum MessageType { - /// Keepalive message, 0 length. - /// Potential Errors if trying to handle a keepalive message like another message. - /// Due to length being 0, should always be explicitly handled. - KeepAlive = u8::MAX, - /// Message telling the client not to send any requests until the peer has unchoked, 1 length. - Choke = 0, - /// Message telling the client that it can send requests, 1 length. - Unchoke = 1, - /// Message indicating that the peer is still interested in downloading, 1 length. - Interested = 2, - /// Message indicating that the peer is not interested in downloading, 1 length. - NotInterested = 3, - /// Message indicating that the peer has a given piece, fixed length. - Have = 4, - /// Message sent after a handshake, represents the pieces that the peer has. - Bitfield = 5, - /// Request a given part of a piece based on index, offset, and length, 13 length. - Request = 6, - /// A response to a request with the accompanying data, varying length. - Piece = 7, - /// Cancels a request, 13 length. - Cancel = 8, - /// Placeholder for unimplemented message type. - Port = 9, - Error -} - -impl From for MessageType { - fn from(val: u8) -> MessageType { - match val { - 0 => MessageType::Choke, - 1 => MessageType::Unchoke, - 2 => MessageType::Interested, - 3 => MessageType::NotInterested, - 4 => MessageType::Have, - 5 => MessageType::Bitfield, - 6 => MessageType::Request, - 7 => MessageType::Piece, - 8 => MessageType::Cancel, - 9 => MessageType::Port, - _ => { - error!("Invalid Message Type: {}", val); - MessageType::Error - } - } - } -} - -impl From for u8 { - fn from(val: MessageType) -> u8 { - match val { - MessageType::Choke => 0, - MessageType::Unchoke => 1, - MessageType::Interested => 2, - MessageType::NotInterested => 3, - MessageType::Have => 4, - MessageType::Bitfield => 5, - MessageType::Request => 6, - MessageType::Piece => 7, - MessageType::Cancel => 8, - MessageType::Port => 9, - _ => { - error!("Invalid Message Type: {:?}", val); - u8::MAX - } - } - } -} \ No newline at end of file diff --git a/src/torrent.rs b/src/torrent.rs deleted file mode 100644 index a4a03e3..0000000 --- a/src/torrent.rs +++ /dev/null @@ -1,249 +0,0 @@ -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)] -struct Node(String, i64); - -/// Represents a file described in a torrent. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct File { - pub path: Vec, - pub length: u64, - #[serde(default)] - md5sum: Option, -} - -/// Represents the metadata of a torrent. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Info { - pub name: String, - #[serde(with = "serde_bytes")] - pub pieces: Vec, - #[serde(rename = "piece length")] - pub piece_length: u64, - #[serde(default)] - md5sum: Option, - #[serde(default)] - pub length: Option, - #[serde(default)] - pub files: Option>, - #[serde(default)] - private: Option, - #[serde(default)] - path: Option>, - #[serde(default)] - #[serde(rename = "root hash")] - root_hash: Option, -} - -/// Represents a torrent. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Torrent { - pub info: Info, - #[serde(default)] - pub announce: Option, - #[serde(default)] - nodes: Option>, - #[serde(default)] - encoding: Option, - #[serde(default)] - httpseeds: Option>, - #[serde(default)] - #[serde(rename = "announce-list")] - announce_list: Option>>, - #[serde(default)] - #[serde(rename = "creation date")] - creation_date: Option, - #[serde(rename = "comment")] - comment: Option, - #[serde(default)] - #[serde(rename = "created by")] - created_by: Option -} - -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 { - 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 = 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 { - 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 { - 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 - }; - - if let Some(files) = &self.info.files { - let mut n = 0; - - for file in files { - n += file.length; - }; - - return n - }; - - 0 - } - - pub fn get_trackers(&self) -> Option> { - 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(); - - 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"); - } - } - - 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 - } - } -} \ No newline at end of file diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs deleted file mode 100644 index d23c619..0000000 --- a/src/tracker/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod tracker; \ No newline at end of file