pub(super) fn split_userinfo(url: &str) -> (String, Option<(String, String)>) {
let Some((scheme, rest)) = url.split_once("://") else {
return (url.to_string(), None);
};
let Some((authority, tail)) = rest.split_once('/') else {
return match rest.rsplit_once('@') {
Some((ui, host)) => (format!("{scheme}://{host}"), parse_userinfo(ui)),
None => (url.to_string(), None),
};
};
match authority.rsplit_once('@') {
Some((ui, host)) => (format!("{scheme}://{host}/{tail}"), parse_userinfo(ui)),
None => (url.to_string(), None),
}
}
fn parse_userinfo(ui: &str) -> Option<(String, String)> {
let (u, p) = ui.split_once(':').unwrap_or((ui, ""));
(!u.is_empty()).then(|| (u.to_string(), p.to_string()))
}
pub(super) enum Challenge {
Basic,
Digest {
realm: String,
nonce: String,
opaque: Option<String>,
qop: Option<String>,
algorithm: Option<String>,
},
}
pub(super) fn parse_challenge(value: &str) -> Option<Challenge> {
let value = value.trim();
let (scheme, params) = value.split_once(char::is_whitespace).unwrap_or((value, ""));
if scheme.eq_ignore_ascii_case("basic") {
return Some(Challenge::Basic);
}
if !scheme.eq_ignore_ascii_case("digest") {
return None;
}
let get = |key: &str| param(params, key);
Some(Challenge::Digest {
realm: get("realm").unwrap_or_default(),
nonce: get("nonce").unwrap_or_default(),
opaque: get("opaque"),
qop: get("qop").and_then(|q| {
q.split(',')
.map(str::trim)
.find(|v| v.eq_ignore_ascii_case("auth"))
.map(|_| "auth".to_string())
}),
algorithm: get("algorithm"),
})
}
fn param(params: &str, key: &str) -> Option<String> {
for part in params.split(',') {
let part = part.trim();
if let Some(rest) = part.strip_prefix(key) {
let rest = rest.trim_start();
if let Some(v) = rest.strip_prefix('=') {
let v = v.trim();
let v = v
.strip_prefix('"')
.and_then(|x| x.strip_suffix('"'))
.unwrap_or(v);
return Some(v.to_string());
}
}
}
None
}
pub(super) fn authorization(
challenge: &Challenge,
user: &str,
pass: &str,
method: &str,
uri: &str,
) -> String {
match challenge {
Challenge::Basic => format!("Basic {}", base64(format!("{user}:{pass}").as_bytes())),
Challenge::Digest {
realm,
nonce,
opaque,
qop,
algorithm,
} => {
let ha1 = hex(&md5(format!("{user}:{realm}:{pass}").as_bytes()));
let ha2 = hex(&md5(format!("{method}:{uri}").as_bytes()));
let mut h = format!(
"Digest username=\"{user}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\", "
);
let response = match qop {
Some(qop) => {
let nc = "00000001";
let cnonce = &hex(&md5(nonce.as_bytes()))[..16];
let resp = hex(&md5(
format!("{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}").as_bytes()
));
h.push_str(&format!(
"qop={qop}, nc={nc}, cnonce=\"{cnonce}\", response=\"{resp}\""
));
return finalize(h, opaque, algorithm);
}
None => hex(&md5(format!("{ha1}:{nonce}:{ha2}").as_bytes())),
};
h.push_str(&format!("response=\"{response}\""));
finalize(h, opaque, algorithm)
}
}
}
fn finalize(mut h: String, opaque: &Option<String>, algorithm: &Option<String>) -> String {
if let Some(a) = algorithm {
h.push_str(&format!(", algorithm={a}"));
}
if let Some(o) = opaque {
h.push_str(&format!(", opaque=\"{o}\""));
}
h
}
fn md5(input: &[u8]) -> [u8; 16] {
#[rustfmt::skip]
const S: [u32; 64] = [
7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20,
4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21,
];
#[rustfmt::skip]
const K: [u32; 64] = [
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391,
];
let (mut a0, mut b0, mut c0, mut d0) = (
0x6745_2301u32,
0xefcd_ab89u32,
0x98ba_dcfeu32,
0x1032_5476u32,
);
let mut msg = input.to_vec();
let bitlen = (input.len() as u64).wrapping_mul(8);
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend_from_slice(&bitlen.to_le_bytes());
for chunk in msg.chunks_exact(64) {
let mut m = [0u32; 16];
for (i, w) in m.iter_mut().enumerate() {
*w = u32::from_le_bytes(chunk[i * 4..i * 4 + 4].try_into().unwrap());
}
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
for i in 0..64 {
let (f, g) = match i {
0..=15 => ((b & c) | (!b & d), i),
16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
_ => (c ^ (b | !d), (7 * i) % 16),
};
let f = f.wrapping_add(a).wrapping_add(K[i]).wrapping_add(m[g]);
a = d;
d = c;
c = b;
b = b.wrapping_add(f.rotate_left(S[i]));
}
a0 = a0.wrapping_add(a);
b0 = b0.wrapping_add(b);
c0 = c0.wrapping_add(c);
d0 = d0.wrapping_add(d);
}
let mut out = [0u8; 16];
out[0..4].copy_from_slice(&a0.to_le_bytes());
out[4..8].copy_from_slice(&b0.to_le_bytes());
out[8..12].copy_from_slice(&c0.to_le_bytes());
out[12..16].copy_from_slice(&d0.to_le_bytes());
out
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn base64(data: &[u8]) -> String {
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
out.push(A[(n >> 18 & 63) as usize] as char);
out.push(A[(n >> 12 & 63) as usize] as char);
out.push(if chunk.len() > 1 {
A[(n >> 6 & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
A[(n & 63) as usize] as char
} else {
'='
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn md5_rfc1321_vectors() {
assert_eq!(hex(&md5(b"")), "d41d8cd98f00b204e9800998ecf8427e");
assert_eq!(hex(&md5(b"abc")), "900150983cd24fb0d6963f7d28e17f72");
assert_eq!(
hex(&md5(b"The quick brown fox jumps over the lazy dog")),
"9e107d9d372bb6826bd81d3542a419d6"
);
}
#[test]
fn base64_known() {
assert_eq!(base64(b"user:pass"), "dXNlcjpwYXNz");
assert_eq!(
base64(b"Aladdin:open sesame"),
"QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
);
}
#[test]
fn splits_userinfo() {
let (url, creds) = split_userinfo("rtsp://admin:1234@cam.local:554/stream");
assert_eq!(url, "rtsp://cam.local:554/stream");
assert_eq!(creds, Some(("admin".into(), "1234".into())));
let (url, creds) = split_userinfo("rtsp://cam.local/stream");
assert_eq!(url, "rtsp://cam.local/stream");
assert_eq!(creds, None);
}
#[test]
fn digest_response_rfc2617() {
let ch = Challenge::Digest {
realm: "testrealm@host.com".into(),
nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093".into(),
opaque: Some("5ccc069c403ebaf9f0171e9517f40e41".into()),
qop: Some("auth".into()),
algorithm: None,
};
let h = authorization(&ch, "Mufasa", "Circle Of Life", "GET", "/dir/index.html");
assert!(h.starts_with("Digest username=\"Mufasa\""));
assert!(h.contains("qop=auth"));
assert!(h.contains("response=\""));
}
}