diff --git a/Cargo.toml b/Cargo.toml index ea5bf45..f4ff11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ async-std = { version = "1.9.0", features = ["attributes"] } async-trait = "0.1.50" futures = "0.3.15" log = "0.4.8" -quickcheck = "1.0.3" +proptest = "1.0.0" simplelog = "0.10.0" -thiserror = "1.0.24" \ No newline at end of file +thiserror = "1.0.24" + +[dev-dependencies] +proptest = "1.0.0" +proptest-derive = "0.3.0" \ No newline at end of file diff --git a/src/messages/authentication_method.rs b/src/messages/authentication_method.rs index 72bf8ff..eedaf8d 100644 --- a/src/messages/authentication_method.rs +++ b/src/messages/authentication_method.rs @@ -5,8 +5,11 @@ use async_std::task; #[cfg(test)] use futures::io::Cursor; use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest::prelude::{Arbitrary, Just, Strategy, prop_oneof}; +#[cfg(test)] +use proptest::strategy::BoxedStrategy; use std::fmt; #[allow(clippy::upper_case_acronyms)] @@ -45,6 +48,31 @@ impl fmt::Display for AuthenticationMethod { } } +#[cfg(test)] +impl Arbitrary for AuthenticationMethod { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> BoxedStrategy { + prop_oneof![ + Just(AuthenticationMethod::None), + Just(AuthenticationMethod::GSSAPI), + Just(AuthenticationMethod::UsernameAndPassword), + Just(AuthenticationMethod::ChallengeHandshake), + Just(AuthenticationMethod::ChallengeResponse), + Just(AuthenticationMethod::SSL), + Just(AuthenticationMethod::NDS), + Just(AuthenticationMethod::MultiAuthenticationFramework), + Just(AuthenticationMethod::JSONPropertyBlock), + Just(AuthenticationMethod::NoAcceptableMethods), + + (0x80u8..=0xfe).prop_map(AuthenticationMethod::PrivateMethod), + ].boxed() + } +} + + + impl AuthenticationMethod { pub async fn read( r: &mut R, @@ -94,28 +122,6 @@ impl AuthenticationMethod { } } -#[cfg(test)] -impl Arbitrary for AuthenticationMethod { - fn arbitrary(g: &mut Gen) -> AuthenticationMethod { - let mut vals = vec![ - AuthenticationMethod::None, - AuthenticationMethod::GSSAPI, - AuthenticationMethod::UsernameAndPassword, - AuthenticationMethod::ChallengeHandshake, - AuthenticationMethod::ChallengeResponse, - AuthenticationMethod::SSL, - AuthenticationMethod::NDS, - AuthenticationMethod::MultiAuthenticationFramework, - AuthenticationMethod::JSONPropertyBlock, - AuthenticationMethod::NoAcceptableMethods, - ]; - for x in 0x80..0xffu8 { - vals.push(AuthenticationMethod::PrivateMethod(x)); - } - g.choose(&vals).unwrap().clone() - } -} - standard_roundtrip!(auth_byte_roundtrips, AuthenticationMethod); #[test] diff --git a/src/messages/client_command.rs b/src/messages/client_command.rs index 148d4da..141dcee 100644 --- a/src/messages/client_command.rs +++ b/src/messages/client_command.rs @@ -10,12 +10,14 @@ use async_std::task; use futures::io::Cursor; use futures::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use log::debug; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest_derive::Arbitrary; #[cfg(test)] use std::net::Ipv4Addr; #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub enum ClientConnectionCommand { EstablishTCPStream, EstablishTCPPortBinding, @@ -23,6 +25,7 @@ pub enum ClientConnectionCommand { } #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub struct ClientConnectionRequest { pub command_code: ClientConnectionCommand, pub destination_address: SOCKSv5Address, @@ -89,33 +92,6 @@ impl ClientConnectionRequest { } } -#[cfg(test)] -impl Arbitrary for ClientConnectionCommand { - fn arbitrary(g: &mut Gen) -> ClientConnectionCommand { - let options = [ - ClientConnectionCommand::EstablishTCPStream, - ClientConnectionCommand::EstablishTCPPortBinding, - ClientConnectionCommand::AssociateUDPPort, - ]; - *g.choose(&options).unwrap() - } -} - -#[cfg(test)] -impl Arbitrary for ClientConnectionRequest { - fn arbitrary(g: &mut Gen) -> Self { - let command_code = ClientConnectionCommand::arbitrary(g); - let destination_address = SOCKSv5Address::arbitrary(g); - let destination_port = u16::arbitrary(g); - - ClientConnectionRequest { - command_code, - destination_address, - destination_port, - } - } -} - standard_roundtrip!(client_request_roundtrips, ClientConnectionRequest); #[test] diff --git a/src/messages/client_greeting.rs b/src/messages/client_greeting.rs index cb655d7..63787a7 100644 --- a/src/messages/client_greeting.rs +++ b/src/messages/client_greeting.rs @@ -8,8 +8,9 @@ use async_std::task; #[cfg(test)] use futures::io::Cursor; use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest_derive::Arbitrary; /// Client greetings are the first message sent in a SOCKSv5 session. They /// identify that there's a client that wants to talk to a server, and that @@ -17,6 +18,7 @@ use quickcheck::{quickcheck, Arbitrary, Gen}; /// said server. (It feels weird that the offer/choice goes this way instead /// of the reverse, but whatever.) #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub struct ClientGreeting { pub acceptable_methods: Vec, } @@ -68,20 +70,6 @@ impl ClientGreeting { } } -#[cfg(test)] -impl Arbitrary for ClientGreeting { - fn arbitrary(g: &mut Gen) -> ClientGreeting { - let amt = u8::arbitrary(g); - let mut acceptable_methods = Vec::with_capacity(amt as usize); - - for _ in 0..amt { - acceptable_methods.push(AuthenticationMethod::arbitrary(g)); - } - - ClientGreeting { acceptable_methods } - } -} - standard_roundtrip!(client_greeting_roundtrips, ClientGreeting); #[test] diff --git a/src/messages/client_username_password.rs b/src/messages/client_username_password.rs index 50cd4d0..d071a82 100644 --- a/src/messages/client_username_password.rs +++ b/src/messages/client_username_password.rs @@ -1,6 +1,4 @@ use crate::errors::{DeserializationError, SerializationError}; -#[cfg(test)] -use crate::messages::utils::arbitrary_socks_string; use crate::serialize::{read_string, write_string}; use crate::standard_roundtrip; #[cfg(test)] @@ -9,7 +7,10 @@ use async_std::task; use futures::io::Cursor; use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest::prelude::{Arbitrary, BoxedStrategy}; +use proptest::proptest; +#[cfg(test)] +use proptest::strategy::Strategy; #[derive(Clone, Debug, Eq, PartialEq)] pub struct ClientUsernamePassword { @@ -17,6 +18,26 @@ pub struct ClientUsernamePassword { pub password: String, } +#[cfg(test)] +const USERNAME_REGEX: &str = "[a-zA-Z0-9~!@#$%^&*_\\-+=:;?<>]+"; +#[cfg(test)] +const PASSWORD_REGEX: &str = "[a-zA-Z0-9~!@#$%^&*_\\-+=:;?<>]+"; + +#[cfg(test)] +impl Arbitrary for ClientUsernamePassword { + type Parameters = Option; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let max_len = args.unwrap_or(12) as usize; + (USERNAME_REGEX, PASSWORD_REGEX).prop_map(move |(mut username, mut password)| { + username.shrink_to(max_len); + password.shrink_to(max_len); + ClientUsernamePassword { username, password } + }).boxed() + } +} + impl ClientUsernamePassword { pub async fn read( r: &mut R, @@ -47,16 +68,6 @@ impl ClientUsernamePassword { } } -#[cfg(test)] -impl Arbitrary for ClientUsernamePassword { - fn arbitrary(g: &mut Gen) -> Self { - let username = arbitrary_socks_string(g); - let password = arbitrary_socks_string(g); - - ClientUsernamePassword { username, password } - } -} - standard_roundtrip!(username_password_roundtrips, ClientUsernamePassword); #[test] diff --git a/src/messages/server_auth_response.rs b/src/messages/server_auth_response.rs index 5db3b83..0963265 100644 --- a/src/messages/server_auth_response.rs +++ b/src/messages/server_auth_response.rs @@ -5,10 +5,12 @@ use async_std::task; #[cfg(test)] use futures::io::Cursor; use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest_derive::Arbitrary; #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub struct ServerAuthResponse { pub success: bool, } @@ -55,14 +57,6 @@ impl ServerAuthResponse { } } -#[cfg(test)] -impl Arbitrary for ServerAuthResponse { - fn arbitrary(g: &mut Gen) -> ServerAuthResponse { - let success = bool::arbitrary(g); - ServerAuthResponse { success } - } -} - standard_roundtrip!(server_auth_response, ServerAuthResponse); #[test] diff --git a/src/messages/server_choice.rs b/src/messages/server_choice.rs index 1691a20..2b452e4 100644 --- a/src/messages/server_choice.rs +++ b/src/messages/server_choice.rs @@ -8,10 +8,12 @@ use async_std::task; #[cfg(test)] use futures::io::Cursor; use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest_derive::Arbitrary; #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub struct ServerChoice { pub chosen_method: AuthenticationMethod, } @@ -56,15 +58,6 @@ impl ServerChoice { } } -#[cfg(test)] -impl Arbitrary for ServerChoice { - fn arbitrary(g: &mut Gen) -> ServerChoice { - ServerChoice { - chosen_method: AuthenticationMethod::arbitrary(g), - } - } -} - standard_roundtrip!(server_choice_roundtrips, ServerChoice); #[test] diff --git a/src/messages/server_response.rs b/src/messages/server_response.rs index 1e60c15..bf35d8f 100644 --- a/src/messages/server_response.rs +++ b/src/messages/server_response.rs @@ -11,12 +11,14 @@ use async_std::task; use futures::io::Cursor; use futures::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use log::warn; +use proptest::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest_derive::Arbitrary; use std::net::Ipv4Addr; use thiserror::Error; #[derive(Clone, Debug, Eq, Error, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub enum ServerResponseStatus { #[error("Actually, everything's fine (weird to see this in an error)")] RequestGranted, @@ -45,6 +47,7 @@ impl IntoErrorResponse for ServerResponseStatus { } #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Arbitrary))] pub struct ServerResponse { pub status: ServerResponseStatus, pub bound_address: SOCKSv5Address, @@ -128,39 +131,6 @@ impl ServerResponse { } } -#[cfg(test)] -impl Arbitrary for ServerResponseStatus { - fn arbitrary(g: &mut Gen) -> ServerResponseStatus { - let options = [ - ServerResponseStatus::RequestGranted, - ServerResponseStatus::GeneralFailure, - ServerResponseStatus::ConnectionNotAllowedByRule, - ServerResponseStatus::NetworkUnreachable, - ServerResponseStatus::HostUnreachable, - ServerResponseStatus::ConnectionRefused, - ServerResponseStatus::TTLExpired, - ServerResponseStatus::CommandNotSupported, - ServerResponseStatus::AddressTypeNotSupported, - ]; - g.choose(&options).unwrap().clone() - } -} - -#[cfg(test)] -impl Arbitrary for ServerResponse { - fn arbitrary(g: &mut Gen) -> Self { - let status = ServerResponseStatus::arbitrary(g); - let bound_address = SOCKSv5Address::arbitrary(g); - let bound_port = u16::arbitrary(g); - - ServerResponse { - status, - bound_address, - bound_port, - } - } -} - standard_roundtrip!(server_response_roundtrips, ServerResponse); #[test] diff --git a/src/messages/utils.rs b/src/messages/utils.rs index 61c0a12..8c44441 100644 --- a/src/messages/utils.rs +++ b/src/messages/utils.rs @@ -1,32 +1,15 @@ -#[cfg(test)] -use quickcheck::{Arbitrary, Gen}; - -#[cfg(test)] -pub fn arbitrary_socks_string(g: &mut Gen) -> String { - loop { - let mut potential = String::arbitrary(g); - - potential.truncate(255); - let bytestring = potential.as_bytes(); - - if !bytestring.is_empty() && bytestring.len() < 256 { - return potential; - } - } -} - #[doc(hidden)] #[macro_export] macro_rules! standard_roundtrip { ($name: ident, $t: ty) => { - #[cfg(test)] - quickcheck! { - fn $name(xs: $t) -> bool { + proptest! { + #[test] + fn $name(xs: $t) { let mut buffer = vec![]; task::block_on(xs.write(&mut buffer)).unwrap(); let mut cursor = Cursor::new(buffer); let ys = <$t>::read(&mut cursor); - xs == task::block_on(ys).unwrap() + assert_eq!(xs, task::block_on(ys).unwrap()); } } }; diff --git a/src/network/address.rs b/src/network/address.rs index 314a06b..deb281d 100644 --- a/src/network/address.rs +++ b/src/network/address.rs @@ -1,6 +1,4 @@ use crate::errors::{DeserializationError, SerializationError}; -#[cfg(test)] -use crate::messages::utils::arbitrary_socks_string; use crate::serialize::{read_amt, read_string, write_string}; use crate::standard_roundtrip; #[cfg(test)] @@ -8,8 +6,9 @@ use async_std::task; #[cfg(test)] use futures::io::Cursor; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use proptest::prelude::proptest; #[cfg(test)] -use quickcheck::{quickcheck, Arbitrary, Gen}; +use proptest::prelude::{Arbitrary, BoxedStrategy, Strategy, any, prop_oneof}; use std::convert::TryFrom; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -22,6 +21,28 @@ pub enum SOCKSv5Address { Name(String), } +#[cfg(test)] +const HOSTNAME_REGEX: &str = "[a-zA-Z0-9_.]+"; + +#[cfg(test)] +impl Arbitrary for SOCKSv5Address { + type Parameters = Option; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + let max_len = args.unwrap_or(32) as usize; + + prop_oneof![ + any::().prop_map(SOCKSv5Address::IP4), + any::().prop_map(SOCKSv5Address::IP6), + HOSTNAME_REGEX.prop_map(move |mut hostname| { + hostname.shrink_to(max_len); + SOCKSv5Address::Name(hostname) + }), + ].boxed() + } +} + #[derive(Error, Debug, PartialEq)] pub enum AddressConversionError { #[error("Couldn't convert IPv4 address into destination type")] @@ -170,28 +191,11 @@ pub trait HasLocalAddress { fn local_addr(&self) -> (SOCKSv5Address, u16); } -#[cfg(test)] -impl Arbitrary for SOCKSv5Address { - fn arbitrary(g: &mut Gen) -> Self { - let ip4 = Ipv4Addr::arbitrary(g); - let ip6 = Ipv6Addr::arbitrary(g); - let nm = arbitrary_socks_string(g); - - g.choose(&[ - SOCKSv5Address::IP4(ip4), - SOCKSv5Address::IP6(ip6), - SOCKSv5Address::Name(nm), - ]) - .unwrap() - .clone() - } -} - standard_roundtrip!(address_roundtrips, SOCKSv5Address); -#[cfg(test)] -quickcheck! { - fn ip_conversion(x: IpAddr) -> bool { +proptest! { + #[test] + fn ip_conversion(x: IpAddr) { match x { IpAddr::V4(ref a) => assert_eq!(Err(AddressConversionError::CouldntConvertIP4), @@ -200,35 +204,37 @@ quickcheck! { assert_eq!(Err(AddressConversionError::CouldntConvertIP6), Ipv4Addr::try_from(SOCKSv5Address::from(*a))), } - x == IpAddr::try_from(SOCKSv5Address::from(x)).unwrap() + assert_eq!(x, IpAddr::try_from(SOCKSv5Address::from(x)).unwrap()); } - fn ip4_conversion(x: Ipv4Addr) -> bool { - x == Ipv4Addr::try_from(SOCKSv5Address::from(x)).unwrap() + #[test] + fn ip4_conversion(x: Ipv4Addr) { + assert_eq!(x, Ipv4Addr::try_from(SOCKSv5Address::from(x)).unwrap()); } - fn ip6_conversion(x: Ipv6Addr) -> bool { - x == Ipv6Addr::try_from(SOCKSv5Address::from(x)).unwrap() + #[test] + fn ip6_conversion(x: Ipv6Addr) { + assert_eq!(x, Ipv6Addr::try_from(SOCKSv5Address::from(x)).unwrap()); } - fn display_matches(x: SOCKSv5Address) -> bool { + #[test] + fn display_matches(x: SOCKSv5Address) { match x { - SOCKSv5Address::IP4(a) => format!("{}", a) == format!("{}", x), - SOCKSv5Address::IP6(a) => format!("{}", a) == format!("{}", x), - SOCKSv5Address::Name(ref a) => *a == x.to_string(), + SOCKSv5Address::IP4(a) => assert_eq!(format!("{}", a), format!("{}", x)), + SOCKSv5Address::IP6(a) => assert_eq!(format!("{}", a), format!("{}", x)), + SOCKSv5Address::Name(ref a) => assert_eq!(*a, x.to_string()), } } - fn bad_read_key(x: u8) -> bool { + #[test] + fn bad_read_key(x: u8) { match x { - 1 => true, - 3 => true, - 4 => true, + 1 | 3 | 4 => {} _ => { let buffer = [x, 0, 1, 2, 9, 10]; let mut cursor = Cursor::new(buffer); let meh = SOCKSv5Address::read(&mut cursor); - Err(DeserializationError::InvalidAddressType(x)) == task::block_on(meh) + assert_eq!(Err(DeserializationError::InvalidAddressType(x)), task::block_on(meh)); } } }