rama_net/user/credentials/
basic.rs1use base64::Engine;
2use base64::engine::general_purpose::STANDARD as ENGINE;
3use rama_core::error::{ErrorContext, OpaqueError};
4use std::borrow::Cow;
5
6#[cfg(feature = "http")]
7use rama_http_types::HeaderValue;
8
9#[derive(Debug, Clone)]
10pub struct Basic {
12 data: BasicData,
13}
14
15#[derive(Debug, Clone)]
16enum BasicData {
17 Username(Cow<'static, str>),
18 Pair {
19 username: Cow<'static, str>,
20 password: Cow<'static, str>,
21 },
22 Decoded {
23 decoded: String,
24 colon_pos: usize,
25 },
26}
27
28impl Basic {
29 pub fn new(
31 username: impl Into<Cow<'static, str>>,
32 password: impl Into<Cow<'static, str>>,
33 ) -> Self {
34 let data = BasicData::Pair {
35 username: username.into(),
36 password: password.into(),
37 };
38 Basic { data }
39 }
40
41 pub fn try_from_header_str(s: impl AsRef<str>) -> Result<Self, OpaqueError> {
44 let value = s.as_ref();
45
46 if value.len() <= BASIC_SCHEME.len() + 1 {
47 return Err(OpaqueError::from_display(
48 "invalid scheme length in basic str",
49 ));
50 }
51 if !value.as_bytes()[..BASIC_SCHEME.len()].eq_ignore_ascii_case(BASIC_SCHEME.as_bytes()) {
52 return Err(OpaqueError::from_display("invalid scheme in basic str"));
53 }
54
55 let bytes = &value.as_bytes()[BASIC_SCHEME.len() + 1..];
56 let non_space_pos = bytes
57 .iter()
58 .position(|b| *b != b' ')
59 .ok_or_else(|| OpaqueError::from_display("missing space separator in basic str"))?;
60 let bytes = &bytes[non_space_pos..];
61
62 let bytes = ENGINE
63 .decode(bytes)
64 .context("failed to decode base64 basic str")?;
65
66 let decoded = String::from_utf8(bytes).context("base64 decoded basic str is not utf-8")?;
67
68 Self::try_from_clear_str(decoded)
69 }
70
71 pub fn try_from_clear_str(s: String) -> Result<Self, OpaqueError> {
74 let colon_pos = s
75 .find(':')
76 .ok_or_else(|| OpaqueError::from_display("missing colon separator in clear str"))?;
77 if colon_pos == 0 {
78 return Err(OpaqueError::from_display(
79 "missing username in basic credential",
80 ));
81 }
82 let data = BasicData::Decoded {
83 decoded: s,
84 colon_pos,
85 };
86 Ok(Basic { data })
87 }
88
89 pub fn as_header_string(&self) -> String {
91 let mut encoded = format!("{BASIC_SCHEME} ");
92
93 match &self.data {
94 BasicData::Username(username) => {
95 let decoded = format!("{username}:");
96 ENGINE.encode_string(&decoded, &mut encoded);
97 }
98 BasicData::Pair { username, password } => {
99 let decoded = format!("{username}:{password}");
100 ENGINE.encode_string(&decoded, &mut encoded);
101 }
102 BasicData::Decoded { decoded, .. } => {
103 ENGINE.encode_string(decoded, &mut encoded);
104 }
105 }
106
107 encoded
108 }
109
110 #[cfg(feature = "http")]
111 pub fn as_header_value(&self) -> HeaderValue {
113 let encoded = self.as_header_string();
114 HeaderValue::from_str(&encoded).expect("inner value should always be valid")
116 }
117
118 pub fn as_clear_string(&self) -> String {
120 match &self.data {
121 BasicData::Username(username) => {
122 format!("{username}:")
123 }
124 BasicData::Pair { username, password } => {
125 format!("{username}:{password}")
126 }
127 BasicData::Decoded { decoded, .. } => decoded.clone(),
128 }
129 }
130
131 pub fn unprotected(username: impl Into<Cow<'static, str>>) -> Self {
133 let data: BasicData = BasicData::Username(username.into());
134 Basic { data }
135 }
136
137 pub fn username(&self) -> &str {
139 match &self.data {
140 BasicData::Username(username) => username,
141 BasicData::Pair { username, .. } => username,
142 BasicData::Decoded { decoded, colon_pos } => &decoded[..*colon_pos],
143 }
144 }
145
146 pub fn password(&self) -> &str {
148 match &self.data {
149 BasicData::Username(_) => "",
150 BasicData::Pair { password, .. } => password,
151 BasicData::Decoded { decoded, colon_pos } => &decoded[*colon_pos + 1..],
152 }
153 }
154}
155
156impl PartialEq<Basic> for Basic {
157 fn eq(&self, other: &Basic) -> bool {
158 self.username() == other.username() && self.password() == other.password()
159 }
160}
161
162impl Eq for Basic {}
163
164pub const BASIC_SCHEME: &str = "Basic";
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn basic_parse_empty() {
173 let value = Basic::try_from_header_str("");
174 assert!(value.is_err());
175 }
176
177 #[test]
178 fn basic_clear_text_empty() {
179 let value = Basic::try_from_clear_str("".to_owned());
180 assert!(value.is_err());
181 }
182
183 #[test]
184 fn basic_missing_username() {
185 let value = Basic::try_from_clear_str(":".to_owned());
186 assert!(value.is_err());
187 }
188
189 #[test]
190 fn basic_header() {
191 let auth = Basic::try_from_header_str("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==").unwrap();
192 assert_eq!(auth.username(), "Aladdin");
193 assert_eq!(auth.password(), "open sesame");
194 assert_eq!(
195 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
196 auth.as_header_string()
197 );
198 }
199
200 #[test]
201 fn basic_header_no_password() {
202 let auth = Basic::try_from_header_str("Basic QWxhZGRpbjo=").unwrap();
203 assert_eq!(auth.username(), "Aladdin");
204 assert_eq!(auth.password(), "");
205 assert_eq!("Basic QWxhZGRpbjo=", auth.as_header_string());
206 }
207
208 #[test]
209 fn basic_clear() {
210 let auth = Basic::try_from_clear_str("Aladdin:open sesame".to_owned()).unwrap();
211 assert_eq!(auth.username(), "Aladdin");
212 assert_eq!(auth.password(), "open sesame");
213 assert_eq!("Aladdin:open sesame", auth.as_clear_string());
214 }
215
216 #[test]
217 fn basic_clear_no_password() {
218 let auth = Basic::try_from_clear_str("Aladdin:".to_owned()).unwrap();
219 assert_eq!(auth.username(), "Aladdin");
220 assert_eq!(auth.password(), "");
221 assert_eq!("Aladdin:", auth.as_clear_string());
222 }
223}