use alloc::{
string::{String, ToString},
vec::Vec,
};
use core::fmt;
use thiserror::Error;
use crate::socks::v5::AUTH_VERSION;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum Socks5CredentialsError {
#[error("SOCKS5 username too long: {0} bytes (max 255)")]
UsernameTooLong(usize),
#[error("SOCKS5 password too long: {0} bytes (max 255)")]
PasswordTooLong(usize),
}
#[derive(Clone)]
pub struct Socks5Credentials {
username: String,
password: String,
}
impl Socks5Credentials {
pub fn new(
username: &str,
password: &str,
) -> Result<Socks5Credentials, Socks5CredentialsError> {
if username.len() > 255 {
return Err(Socks5CredentialsError::UsernameTooLong(username.len()));
}
if password.len() > 255 {
return Err(Socks5CredentialsError::PasswordTooLong(password.len()));
}
Ok(Socks5Credentials {
username: username.to_string(),
password: password.to_string(),
})
}
pub(crate) fn encode(&self) -> Vec<u8> {
let user = self.username.as_bytes();
let pass = self.password.as_bytes();
let mut out = Vec::with_capacity(3 + user.len() + pass.len());
out.push(AUTH_VERSION);
out.push(user.len() as u8);
out.extend_from_slice(user);
out.push(pass.len() as u8);
out.extend_from_slice(pass);
out
}
}
impl fmt::Debug for Socks5Credentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Socks5Credentials")
.field("username", &self.username)
.field("password", &"***")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_matches_rfc1929() {
let creds = Socks5Credentials::new("user", "pass").unwrap();
assert_eq!(
creds.encode(),
[
0x01, 0x04, b'u', b's', b'e', b'r', 0x04, b'p', b'a', b's', b's'
]
);
}
#[test]
fn rejects_overlong_fields() {
let long = "x".repeat(256);
assert!(matches!(
Socks5Credentials::new(&long, "p"),
Err(Socks5CredentialsError::UsernameTooLong(256))
));
assert!(matches!(
Socks5Credentials::new("u", &long),
Err(Socks5CredentialsError::PasswordTooLong(256))
));
}
#[test]
fn debug_redacts_password() {
let creds = Socks5Credentials::new("alice", "secret").unwrap();
let rendered = format!("{creds:?}");
assert!(rendered.contains("alice"));
assert!(!rendered.contains("secret"));
}
}