use crate::error::{Error, Result};
const MAX_SEGMENT_LENGTH: usize = 512;
fn is_safe_path_segment(s: &str) -> bool {
!s.is_empty()
&& s.len() <= MAX_SEGMENT_LENGTH
&& !s.contains("..")
&& !s.contains(['/', '\\', '\0', '?', '#', '%'])
&& !s.chars().any(char::is_whitespace)
}
fn is_ascii_digits(s: &str) -> bool {
!s.is_empty() && s.len() <= MAX_SEGMENT_LENGTH && s.bytes().all(|b| b.is_ascii_digit())
}
fn validate_path_segment(value: &str, make_err: impl FnOnce(String) -> Error) -> Result<()> {
if is_safe_path_segment(value) {
Ok(())
} else {
Err(make_err(value.to_owned()))
}
}
fn validate_digits(value: &str, make_err: impl FnOnce(String) -> Error) -> Result<()> {
if is_ascii_digits(value) {
Ok(())
} else {
Err(make_err(value.to_owned()))
}
}
pub(crate) fn validate_data_source(ds: &str) -> Result<()> {
validate_path_segment(ds, Error::InvalidDataSource)
}
pub(crate) fn validate_username(username: &str) -> Result<()> {
validate_path_segment(username, Error::InvalidUsername)
}
pub(crate) fn validate_connection_id(id: &str) -> Result<()> {
validate_digits(id, Error::InvalidConnectionId)
}
pub(crate) fn validate_sharing_profile_id(id: &str) -> Result<()> {
validate_digits(id, Error::InvalidSharingProfileId)
}
pub(crate) fn validate_user_group_id(id: &str) -> Result<()> {
validate_path_segment(id, Error::InvalidUserGroupId)
}
pub(crate) fn validate_connection_group_id(id: &str) -> Result<()> {
validate_path_segment(id, Error::InvalidConnectionGroupId)
}
pub(crate) fn validate_tunnel_id(id: &str) -> Result<()> {
validate_path_segment(id, Error::InvalidTunnelId)
}
pub(crate) fn validate_query_param(name: &str, value: &str) -> Result<()> {
if value.is_empty()
|| value.len() > MAX_SEGMENT_LENGTH
|| value.contains(['&', '#', '?', '%', '\0'])
|| value.bytes().any(|b| b.is_ascii_control())
{
Err(Error::InvalidQueryParam {
name: name.to_owned(),
reason: if value.is_empty() {
"must not be empty".to_owned()
} else if value.len() > MAX_SEGMENT_LENGTH {
"exceeds maximum length".to_owned()
} else {
"contains unsafe characters".to_owned()
},
})
} else {
Ok(())
}
}
pub(crate) fn validate_token(token: &str) -> Result<()> {
if token.is_empty()
|| token.len() > MAX_SEGMENT_LENGTH
|| token.contains("..")
|| token.contains(['/', '\\', '\0', '?', '#', '%', '&'])
|| token.chars().any(char::is_whitespace)
{
Err(Error::InvalidToken(token.to_owned()))
} else {
Ok(())
}
}
pub(crate) fn validate_sort_order(order: &str) -> Result<()> {
match order {
"asc" | "desc" => Ok(()),
_ => Err(Error::InvalidQueryParam {
name: "order".to_owned(),
reason: format!("must be \"asc\" or \"desc\", got \"{order}\""),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_data_source() {
assert!(validate_data_source("mysql").is_ok());
assert!(validate_data_source("postgresql").is_ok());
assert!(validate_data_source("my-data-source").is_ok());
}
#[test]
fn invalid_data_source_empty() {
assert!(matches!(
validate_data_source(""),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_slash() {
assert!(matches!(
validate_data_source("a/b"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_backslash() {
assert!(matches!(
validate_data_source("a\\b"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_dot_dot() {
assert!(matches!(
validate_data_source(".."),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_null() {
assert!(matches!(
validate_data_source("a\0b"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_query() {
assert!(matches!(
validate_data_source("a?b"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_hash() {
assert!(matches!(
validate_data_source("a#b"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn invalid_data_source_percent() {
assert!(matches!(
validate_data_source("a%2Fb"),
Err(Error::InvalidDataSource(_))
));
}
#[test]
fn valid_username() {
assert!(validate_username("guacadmin").is_ok());
assert!(validate_username("john.doe").is_ok());
assert!(validate_username("user-1").is_ok());
}
#[test]
fn invalid_username_empty() {
assert!(matches!(
validate_username(""),
Err(Error::InvalidUsername(_))
));
}
#[test]
fn invalid_username_slash() {
assert!(matches!(
validate_username("../admin"),
Err(Error::InvalidUsername(_))
));
}
#[test]
fn invalid_username_null() {
assert!(matches!(
validate_username("user\0"),
Err(Error::InvalidUsername(_))
));
}
#[test]
fn valid_connection_id() {
assert!(validate_connection_id("1").is_ok());
assert!(validate_connection_id("42").is_ok());
assert!(validate_connection_id("12345").is_ok());
}
#[test]
fn invalid_connection_id_empty() {
assert!(matches!(
validate_connection_id(""),
Err(Error::InvalidConnectionId(_))
));
}
#[test]
fn invalid_connection_id_non_digit() {
assert!(matches!(
validate_connection_id("abc"),
Err(Error::InvalidConnectionId(_))
));
}
#[test]
fn invalid_connection_id_mixed() {
assert!(matches!(
validate_connection_id("12abc"),
Err(Error::InvalidConnectionId(_))
));
}
#[test]
fn invalid_connection_id_path_traversal() {
assert!(matches!(
validate_connection_id("../../etc"),
Err(Error::InvalidConnectionId(_))
));
}
#[test]
fn valid_sharing_profile_id() {
assert!(validate_sharing_profile_id("1").is_ok());
assert!(validate_sharing_profile_id("99").is_ok());
}
#[test]
fn invalid_sharing_profile_id_empty() {
assert!(matches!(
validate_sharing_profile_id(""),
Err(Error::InvalidSharingProfileId(_))
));
}
#[test]
fn invalid_sharing_profile_id_non_digit() {
assert!(matches!(
validate_sharing_profile_id("abc"),
Err(Error::InvalidSharingProfileId(_))
));
}
#[test]
fn valid_user_group_id() {
assert!(validate_user_group_id("admins").is_ok());
assert!(validate_user_group_id("my-group").is_ok());
}
#[test]
fn invalid_user_group_id_empty() {
assert!(matches!(
validate_user_group_id(""),
Err(Error::InvalidUserGroupId(_))
));
}
#[test]
fn invalid_user_group_id_slash() {
assert!(matches!(
validate_user_group_id("a/b"),
Err(Error::InvalidUserGroupId(_))
));
}
#[test]
fn invalid_user_group_id_dot_dot() {
assert!(matches!(
validate_user_group_id(".."),
Err(Error::InvalidUserGroupId(_))
));
}
#[test]
fn valid_connection_group_id() {
assert!(validate_connection_group_id("ROOT").is_ok());
assert!(validate_connection_group_id("1").is_ok());
assert!(validate_connection_group_id("42").is_ok());
}
#[test]
fn invalid_connection_group_id_empty() {
assert!(matches!(
validate_connection_group_id(""),
Err(Error::InvalidConnectionGroupId(_))
));
}
#[test]
fn invalid_connection_group_id_slash() {
assert!(matches!(
validate_connection_group_id("a/b"),
Err(Error::InvalidConnectionGroupId(_))
));
}
#[test]
fn invalid_connection_group_id_dot_dot() {
assert!(matches!(
validate_connection_group_id(".."),
Err(Error::InvalidConnectionGroupId(_))
));
}
#[test]
fn valid_tunnel_id() {
assert!(validate_tunnel_id("abc-123").is_ok());
assert!(validate_tunnel_id("tunnel-1").is_ok());
}
#[test]
fn invalid_tunnel_id_empty() {
assert!(matches!(
validate_tunnel_id(""),
Err(Error::InvalidTunnelId(_))
));
}
#[test]
fn invalid_tunnel_id_slash() {
assert!(matches!(
validate_tunnel_id("a/b"),
Err(Error::InvalidTunnelId(_))
));
}
#[test]
fn invalid_tunnel_id_dot_dot() {
assert!(matches!(
validate_tunnel_id(".."),
Err(Error::InvalidTunnelId(_))
));
}
#[test]
fn valid_query_param() {
assert!(validate_query_param("contains", "my-server").is_ok());
assert!(validate_query_param("contains", "test value").is_ok());
}
#[test]
fn invalid_query_param_empty() {
assert!(matches!(
validate_query_param("contains", ""),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_ampersand() {
assert!(matches!(
validate_query_param("contains", "x&limit=0"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_hash() {
assert!(matches!(
validate_query_param("contains", "x#fragment"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_question_mark() {
assert!(matches!(
validate_query_param("contains", "x?extra=1"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_percent() {
assert!(matches!(
validate_query_param("contains", "x%00"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_null() {
assert!(matches!(
validate_query_param("contains", "x\0y"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn invalid_query_param_control_chars() {
assert!(matches!(
validate_query_param("contains", "x\ty"),
Err(Error::InvalidQueryParam { .. })
));
assert!(matches!(
validate_query_param("contains", "x\ny"),
Err(Error::InvalidQueryParam { .. })
));
assert!(matches!(
validate_query_param("contains", "x\ry"),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn valid_token() {
assert!(validate_token("ABCDEF1234567890").is_ok());
assert!(validate_token("168F8D0A2D68247F30B7E2E01187AEE2CF82186D").is_ok());
assert!(validate_token("simple-token").is_ok());
}
#[test]
fn invalid_token_empty() {
assert!(matches!(validate_token(""), Err(Error::InvalidToken(_))));
}
#[test]
fn invalid_token_ampersand() {
assert!(matches!(
validate_token("a&b=c"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_slash() {
assert!(matches!(
validate_token("a/b"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_backslash() {
assert!(matches!(
validate_token("a\\b"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_dot_dot() {
assert!(matches!(
validate_token(".."),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_hash() {
assert!(matches!(
validate_token("tok#frag"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_question_mark() {
assert!(matches!(
validate_token("tok?extra"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_percent() {
assert!(matches!(
validate_token("tok%00"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_null() {
assert!(matches!(
validate_token("tok\0"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn invalid_token_whitespace() {
assert!(matches!(
validate_token("tok en"),
Err(Error::InvalidToken(_))
));
assert!(matches!(
validate_token("\t"),
Err(Error::InvalidToken(_))
));
}
#[test]
fn valid_sort_order() {
assert!(validate_sort_order("asc").is_ok());
assert!(validate_sort_order("desc").is_ok());
}
#[test]
fn invalid_sort_order() {
assert!(matches!(
validate_sort_order("ASC"),
Err(Error::InvalidQueryParam { .. })
));
assert!(matches!(
validate_sort_order("invalid"),
Err(Error::InvalidQueryParam { .. })
));
assert!(matches!(
validate_sort_order(""),
Err(Error::InvalidQueryParam { .. })
));
}
#[test]
fn validation_unicode_inputs() {
assert!(validate_data_source("źródło").is_ok());
assert!(validate_username("użytkownik").is_ok());
assert!(validate_connection_id("٤٢").is_err()); assert!(validate_connection_id("12").is_err()); }
#[test]
fn validation_whitespace_only() {
assert!(validate_data_source(" ").is_err());
assert!(validate_username("\t").is_err());
assert!(validate_connection_id(" ").is_err());
}
#[test]
fn validation_very_long_strings() {
let long = "a".repeat(10_000);
assert!(validate_data_source(&long).is_err());
assert!(validate_username(&long).is_err());
assert!(validate_token(&long).is_err());
assert!(validate_query_param("k", &long).is_err());
let long_digits = "1".repeat(10_000);
assert!(validate_connection_id(&long_digits).is_err());
assert!(validate_sharing_profile_id(&long_digits).is_err());
}
#[test]
fn validation_max_length() {
let at_limit = "a".repeat(MAX_SEGMENT_LENGTH);
assert!(validate_data_source(&at_limit).is_ok());
assert!(validate_username(&at_limit).is_ok());
assert!(validate_token(&at_limit).is_ok());
assert!(validate_query_param("k", &at_limit).is_ok());
let digits_at_limit = "1".repeat(MAX_SEGMENT_LENGTH);
assert!(validate_connection_id(&digits_at_limit).is_ok());
assert!(validate_sharing_profile_id(&digits_at_limit).is_ok());
let over_limit = "a".repeat(MAX_SEGMENT_LENGTH + 1);
assert!(validate_data_source(&over_limit).is_err());
assert!(validate_username(&over_limit).is_err());
assert!(validate_token(&over_limit).is_err());
assert!(validate_query_param("k", &over_limit).is_err());
let digits_over_limit = "1".repeat(MAX_SEGMENT_LENGTH + 1);
assert!(validate_connection_id(&digits_over_limit).is_err());
assert!(validate_sharing_profile_id(&digits_over_limit).is_err());
}
#[test]
fn validation_dot_variants() {
assert!(validate_data_source(".").is_ok());
assert!(validate_data_source("..").is_err());
assert!(validate_data_source("...").is_err());
assert!(validate_data_source("a..b").is_err());
}
#[test]
fn connection_id_leading_zeros() {
assert!(validate_connection_id("007").is_ok());
assert!(validate_connection_id("0").is_ok());
}
#[test]
fn connection_id_negative_decimal_plus() {
assert!(validate_connection_id("-1").is_err());
assert!(validate_connection_id("1.5").is_err());
assert!(validate_connection_id("+1").is_err());
}
}