Skip to content

Commit

Permalink
refactor(http): [#160] extract functions for percent decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Feb 9, 2023
1 parent 0dc3050 commit 7dc4838
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 95 deletions.
22 changes: 9 additions & 13 deletions src/http/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::Arc;
use warp::{reject, Filter, Rejection};

use super::error::Error;
use super::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
use super::{request, WebResult};
use crate::protocol::common::MAX_SCRAPE_TORRENTS;
use crate::protocol::info_hash::InfoHash;
Expand Down Expand Up @@ -78,9 +79,11 @@ fn info_hashes(raw_query: &String) -> WebResult<Vec<InfoHash>> {

for v in split_raw_query {
if v.contains("info_hash") {
// get raw percent encoded infohash
let raw_info_hash = v.split('=').collect::<Vec<&str>>()[1];
let info_hash_bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::<Vec<u8>>();
let info_hash = InfoHash::from_str(&hex::encode(info_hash_bytes));

let info_hash = percent_decode_info_hash(raw_info_hash);

if let Ok(ih) = info_hash {
info_hashes.push(ih);
}
Expand Down Expand Up @@ -112,24 +115,17 @@ fn peer_id(raw_query: &String) -> WebResult<peer::Id> {
for v in split_raw_query {
// look for the peer_id param
if v.contains("peer_id") {
// get raw percent_encoded peer_id
// get raw percent encoded peer id
let raw_peer_id = v.split('=').collect::<Vec<&str>>()[1];

// decode peer_id
let peer_id_bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::<Vec<u8>>();

// peer_id must be 20 bytes
if peer_id_bytes.len() != 20 {
if let Ok(id) = percent_decode_peer_id(raw_peer_id) {
peer_id = Some(id);
} else {
return Err(reject::custom(Error::InvalidPeerId {
location: Location::caller(),
}));
}

// clone peer_id_bytes into fixed length array
let mut byte_arr: [u8; 20] = Default::default();
byte_arr.clone_from_slice(peer_id_bytes.as_slice());

peer_id = Some(peer::Id(byte_arr));
break;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod axum;
pub mod error;
pub mod filters;
pub mod handlers;
pub mod percent_encoding;
pub mod request;
pub mod response;
pub mod routes;
Expand Down
66 changes: 66 additions & 0 deletions src/http/percent_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::protocol::info_hash::{ConversionError, InfoHash};
use crate::tracker::peer::{self, IdConversionError};

/// # Errors
///
/// Will return `Err` if if the decoded bytes do not represent a valid `InfoHash`.
pub fn percent_decode_info_hash(raw_info_hash: &str) -> Result<InfoHash, ConversionError> {
let bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::<Vec<u8>>();
InfoHash::try_from(bytes)
}

/// # Errors
///
/// Will return `Err` if if the decoded bytes do not represent a valid `peer::Id`.
pub fn percent_decode_peer_id(raw_peer_id: &str) -> Result<peer::Id, IdConversionError> {
let bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::<Vec<u8>>();
peer::Id::try_from(bytes)
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
use crate::protocol::info_hash::InfoHash;
use crate::tracker::peer;

#[test]
fn it_should_decode_a_percent_encoded_info_hash() {
let encoded_infohash = "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0";

let info_hash = percent_decode_info_hash(encoded_infohash).unwrap();

assert_eq!(
info_hash,
InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap()
);
}

#[test]
fn it_should_fail_decoding_an_invalid_percent_encoded_info_hash() {
let invalid_encoded_infohash = "invalid percent-encoded infohash";

let info_hash = percent_decode_info_hash(invalid_encoded_infohash);

assert!(info_hash.is_err());
}

#[test]
fn it_should_decode_a_percent_encoded_peer_id() {
let encoded_peer_id = "%2DqB00000000000000000";

let peer_id = percent_decode_peer_id(encoded_peer_id).unwrap();

assert_eq!(peer_id, peer::Id(*b"-qB00000000000000000"));
}

#[test]
fn it_should_fail_decoding_an_invalid_percent_encoded_peer_id() {
let invalid_encoded_peer_id = "invalid percent-encoded peer id";

let peer_id = percent_decode_peer_id(invalid_encoded_peer_id);

assert!(peer_id.is_err());
}
}
71 changes: 71 additions & 0 deletions src/protocol/info_hash.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
use std::panic::Location;

use thiserror::Error;

#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct InfoHash(pub [u8; 20]);

const INFO_HASH_BYTES_LEN: usize = 20;

impl InfoHash {
/// # Panics
///
/// Will panic if byte slice does not contains the exact amount of bytes need for the `InfoHash`.
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), INFO_HASH_BYTES_LEN);
let mut ret = Self([0u8; INFO_HASH_BYTES_LEN]);
ret.0.clone_from_slice(bytes);
ret
}

/// For readability, when accessing the bytes array
#[must_use]
pub fn bytes(&self) -> [u8; 20] {
Expand Down Expand Up @@ -57,6 +74,40 @@ impl std::convert::From<[u8; 20]> for InfoHash {
}
}

#[derive(Error, Debug)]
pub enum ConversionError {
#[error("not enough bytes for infohash: {message} {location}")]
NotEnoughBytes {
location: &'static Location<'static>,
message: String,
},
#[error("too many bytes for infohash: {message} {location}")]
TooManyBytes {
location: &'static Location<'static>,
message: String,
},
}

impl TryFrom<Vec<u8>> for InfoHash {
type Error = ConversionError;

fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
if bytes.len() < INFO_HASH_BYTES_LEN {
return Err(ConversionError::NotEnoughBytes {
location: Location::caller(),
message: format! {"got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN},
});
}
if bytes.len() > INFO_HASH_BYTES_LEN {
return Err(ConversionError::TooManyBytes {
location: Location::caller(),
message: format! {"got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN},
});
}
Ok(Self::from_bytes(&bytes))
}
}

impl serde::ser::Serialize for InfoHash {
fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut buffer = [0u8; 40];
Expand Down Expand Up @@ -166,6 +217,26 @@ mod tests {
);
}

#[test]
fn an_info_hash_can_be_created_from_a_byte_vector() {
let info_hash: InfoHash = [255u8; 20].to_vec().try_into().unwrap();

assert_eq!(
info_hash,
InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()
);
}

#[test]
fn it_should_fail_trying_to_create_an_info_hash_from_a_byte_vector_with_less_than_20_bytes() {
assert!(InfoHash::try_from([255u8; 19].to_vec()).is_err());
}

#[test]
fn it_should_fail_trying_to_create_an_info_hash_from_a_byte_vector_with_more_than_20_bytes() {
assert!(InfoHash::try_from([255u8; 21].to_vec()).is_err());
}

#[test]
fn an_info_hash_can_be_serialized() {
let s = ContainingInfoHash {
Expand Down
134 changes: 134 additions & 0 deletions src/tracker/peer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::net::{IpAddr, SocketAddr};
use std::panic::Location;

use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes};
use serde;
use serde::Serialize;
use thiserror::Error;

use crate::http::request::Announce;
use crate::protocol::clock::{Current, DurationSinceUnixEpoch, Time};
Expand Down Expand Up @@ -91,6 +93,69 @@ impl Peer {
#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)]
pub struct Id(pub [u8; 20]);

const PEER_ID_BYTES_LEN: usize = 20;

#[derive(Error, Debug)]
pub enum IdConversionError {
#[error("not enough bytes for peer id: {message} {location}")]
NotEnoughBytes {
location: &'static Location<'static>,
message: String,
},
#[error("too many bytes for peer id: {message} {location}")]
TooManyBytes {
location: &'static Location<'static>,
message: String,
},
}

impl Id {
/// # Panics
///
/// Will panic if byte slice does not contains the exact amount of bytes need for the `Id`.
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), PEER_ID_BYTES_LEN);
let mut ret = Self([0u8; PEER_ID_BYTES_LEN]);
ret.0.clone_from_slice(bytes);
ret
}
}

impl From<[u8; 20]> for Id {
fn from(bytes: [u8; 20]) -> Self {
Id(bytes)
}
}

impl TryFrom<Vec<u8>> for Id {
type Error = IdConversionError;

fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
if bytes.len() < PEER_ID_BYTES_LEN {
return Err(IdConversionError::NotEnoughBytes {
location: Location::caller(),
message: format! {"got {} bytes, expected {}", bytes.len(), PEER_ID_BYTES_LEN},
});
}
if bytes.len() > PEER_ID_BYTES_LEN {
return Err(IdConversionError::TooManyBytes {
location: Location::caller(),
message: format! {"got {} bytes, expected {}", bytes.len(), PEER_ID_BYTES_LEN},
});
}
Ok(Self::from_bytes(&bytes))
}
}

impl std::str::FromStr for Id {
type Err = IdConversionError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s.as_bytes().to_vec())
}
}

impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.to_hex_string() {
Expand Down Expand Up @@ -239,6 +304,75 @@ mod test {
mod torrent_peer_id {
use crate::tracker::peer;

#[test]
fn should_be_instantiated_from_a_byte_slice() {
let id = peer::Id::from_bytes(&[
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]);

let expected_id = peer::Id([
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]);

assert_eq!(id, expected_id);
}

#[test]
#[should_panic]
fn should_fail_trying_to_instantiate_from_a_byte_slice_with_less_than_20_bytes() {
let less_than_20_bytes = [0; 19];
let _ = peer::Id::from_bytes(&less_than_20_bytes);
}

#[test]
#[should_panic]
fn should_fail_trying_to_instantiate_from_a_byte_slice_with_more_than_20_bytes() {
let more_than_20_bytes = [0; 21];
let _ = peer::Id::from_bytes(&more_than_20_bytes);
}

#[test]
fn should_be_converted_from_a_20_byte_array() {
let id = peer::Id::from([
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]);

let expected_id = peer::Id([
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]);

assert_eq!(id, expected_id);
}

#[test]
fn should_be_converted_from_a_byte_vector() {
let id = peer::Id::try_from(
[
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]
.to_vec(),
)
.unwrap();

let expected_id = peer::Id([
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
]);

assert_eq!(id, expected_id);
}

#[test]
#[should_panic]
fn should_fail_trying_to_convert_from_a_byte_vector_with_less_than_20_bytes() {
let _ = peer::Id::try_from([0; 19].to_vec()).unwrap();
}

#[test]
#[should_panic]
fn should_fail_trying_to_convert_from_a_byte_vector_with_more_than_20_bytes() {
let _ = peer::Id::try_from([0; 21].to_vec()).unwrap();
}

#[test]
fn should_be_converted_to_hex_string() {
let id = peer::Id(*b"-qB00000000000000000");
Expand Down
15 changes: 0 additions & 15 deletions tests/http/bencode.rs

This file was deleted.

Loading

0 comments on commit 7dc4838

Please sign in to comment.