use crate::helpers::{IntoError, Res};
pub const METHOD_NO_AUTH: u8 = 0x00;
pub const METHOD_USER_PASS: u8 = 0x02;
pub const METHOD_NONE_ACCEPTABLE: u8 = 0xFF;
pub fn select_method(offered: &[u8], creds: Option<&Credentials>) -> u8 {
match creds {
None => METHOD_NO_AUTH,
Some(_) if offered.contains(&METHOD_USER_PASS) => METHOD_USER_PASS,
Some(_) => METHOD_NONE_ACCEPTABLE,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
pub username: String,
pub password: String,
}
impl Credentials {
pub fn from_data(data: &[u8]) -> Res<Credentials> {
if data.len() < 2 {
return "Auth too short: need at least a version and a username length.".into_error();
}
if data[0] != 0x01 {
return "Bad auth sub-negotiation version.".into_error();
}
let ulen = usize::from(data[1]);
let uname_end = 2 + ulen;
if data.len() < uname_end + 1 {
return "Auth too short for the stated username length.".into_error();
}
let plen = usize::from(data[uname_end]);
let passwd_end = uname_end + 1 + plen;
if data.len() < passwd_end {
return "Auth too short for the stated password length.".into_error();
}
let username = match String::from_utf8(data[2..uname_end].to_vec()) {
Ok(s) => s,
Err(_) => return "Username is not valid UTF-8.".into_error(),
};
let password = match String::from_utf8(data[uname_end + 1..passwd_end].to_vec()) {
Ok(s) => s,
Err(_) => return "Password is not valid UTF-8.".into_error(),
};
Ok(Credentials { username, password })
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn parses_username_and_password() {
let data = [0x01, 0x03, b'b', b'o', b'b', 0x04, b'p', b'a', b's', b's'];
let creds = Credentials::from_data(&data).unwrap();
assert_eq!(creds.username, "bob");
assert_eq!(creds.password, "pass");
}
#[test]
fn parses_empty_username_and_password() {
let creds = Credentials::from_data(&[0x01, 0x00, 0x00]).unwrap();
assert_eq!(creds.username, "");
assert_eq!(creds.password, "");
}
#[test]
fn rejects_wrong_subnegotiation_version() {
assert!(Credentials::from_data(&[0x05, 0x01, b'a', 0x01, b'b']).is_err());
}
#[test]
fn rejects_truncated_header() {
assert!(Credentials::from_data(&[0x01]).is_err());
assert!(Credentials::from_data(&[]).is_err());
}
#[test]
fn rejects_truncated_username() {
assert!(Credentials::from_data(&[0x01, 0x05, b'a', b'b']).is_err());
}
#[test]
fn rejects_truncated_password() {
assert!(Credentials::from_data(&[0x01, 0x03, b'b', b'o', b'b', 0x04, b'p', b'a']).is_err());
}
#[test]
fn rejects_non_utf8_username() {
assert!(Credentials::from_data(&[0x01, 0x01, 0xFF, 0x01, b'p']).is_err());
}
#[test]
fn selects_no_auth_when_unconfigured() {
assert_eq!(select_method(&[METHOD_NO_AUTH], None), METHOD_NO_AUTH);
assert_eq!(select_method(&[METHOD_USER_PASS], None), METHOD_NO_AUTH);
}
#[test]
fn selects_user_pass_when_configured_and_offered() {
let creds = Credentials {
username: "u".to_owned(),
password: "p".to_owned(),
};
assert_eq!(select_method(&[METHOD_NO_AUTH, METHOD_USER_PASS], Some(&creds)), METHOD_USER_PASS);
}
#[test]
fn rejects_when_configured_but_user_pass_not_offered() {
let creds = Credentials {
username: "u".to_owned(),
password: "p".to_owned(),
};
assert_eq!(select_method(&[METHOD_NO_AUTH], Some(&creds)), METHOD_NONE_ACCEPTABLE);
}
#[test]
fn credentials_compare_on_both_fields() {
let a = Credentials {
username: "u".to_owned(),
password: "p".to_owned(),
};
assert_eq!(
a,
Credentials {
username: "u".to_owned(),
password: "p".to_owned()
}
);
assert_ne!(
a,
Credentials {
username: "u".to_owned(),
password: "x".to_owned()
}
);
assert_ne!(
a,
Credentials {
username: "x".to_owned(),
password: "p".to_owned()
}
);
}
}