use core::{fmt, iter::Peekable};
use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
list,
param::{MAX_CHECKED_PARAMS, Names},
scan::control_kind,
text::{Word, is_token, parse_word, skip_ows, trim_ows},
write::{CountSink, Sink, reserve, unique_names, write_quoted, write_word},
};
const RELAXED: Limits = Limits::DEFAULT
.max_list_members(u16::MAX)
.max_empty_elements(u16::MAX);
fn is_token68(s: &[u8]) -> bool {
let body = s.iter().take_while(|&&b| class::is_token68_char(b)).count();
body > 0
&& s.get(body..)
.is_some_and(|pad| pad.iter().all(|&b| b == b'='))
}
fn auth_param(e: &[u8]) -> Result<(&[u8], Word<'_>)> {
let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
if n == 0 {
return Err(refused(e, 0, ErrorKind::NotToken));
}
let name = e.get(..n).unwrap_or(&[]);
let mut i = n + skip_ows(e.get(n..).unwrap_or(&[]));
if e.get(i) != Some(&b'=') {
return Err(refused(e, i, ErrorKind::MissingValue));
}
i += 1;
i += skip_ows(e.get(i..).unwrap_or(&[]));
let (w, used) = parse_word(e.get(i..).unwrap_or(&[])).map_err(|x| match x.kind() {
ErrorKind::Empty => refused(e, i, ErrorKind::MissingValue),
_ => x.shifted(i),
})?;
if i + used != e.len() {
return Err(refused(e, i + used, ErrorKind::TrailingData));
}
Ok((name, w))
}
fn refused(e: &[u8], at: usize, kind: ErrorKind) -> ParseError {
match e.get(at) {
Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
_ => ParseError::new(kind, at),
}
}
fn is_param_start(e: &[u8]) -> bool {
let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
n > 0 && e.get(n + skip_ows(e.get(n..).unwrap_or(&[]))) == Some(&b'=')
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct AuthParams<'a> {
text: &'a [u8],
count: usize,
}
impl<'a> AuthParams<'a> {
pub fn iter(&self) -> impl Iterator<Item = (&'a [u8], Word<'a>)> + use<'a> {
list::parse(self.text, &RELAXED)
.into_iter()
.flat_map(|l| l.into_iter())
.filter_map(|e| auth_param(e).ok())
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Word<'a>> {
self.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
.map(|(_, v)| v)
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
impl fmt::Debug for AuthParams<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("AuthParams(")?;
f.debug_list()
.entries(
self.iter()
.map(|(n, _)| core::str::from_utf8(n).unwrap_or("?")),
)
.finish()?;
f.write_str(")")
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthPayload<'a> {
None,
Token68(&'a [u8]),
Params(AuthParams<'a>),
}
impl fmt::Debug for AuthPayload<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthPayload::None => f.write_str("None"),
AuthPayload::Token68(_) => f.write_str("Token68(..)"),
AuthPayload::Params(p) => fmt::Debug::fmt(p, f),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Credentials<'a> {
scheme: &'a [u8],
payload: AuthPayload<'a>,
}
impl<'a> Credentials<'a> {
#[must_use]
pub const fn scheme(&self) -> &'a [u8] {
self.scheme
}
#[must_use]
pub fn scheme_is(&self, s: &str) -> bool {
self.scheme.eq_ignore_ascii_case(s.as_bytes())
}
#[must_use]
pub const fn payload(&self) -> AuthPayload<'a> {
self.payload
}
}
impl fmt::Debug for Credentials<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.payload == AuthPayload::None {
return f.debug_struct("Credentials").finish_non_exhaustive();
}
f.debug_struct("Credentials")
.field("scheme", &core::str::from_utf8(self.scheme).unwrap_or("?"))
.finish_non_exhaustive()
}
}
fn check_params(text: &[u8], at: usize, limits: &Limits) -> Result<usize> {
let l = list::parse(text, limits).map_err(|e| e.shifted(at))?;
let max = usize::from(limits.params).min(MAX_CHECKED_PARAMS);
let (mut count, mut names) = (0usize, Names::new());
for e in l.iter() {
let rel = (e.as_ptr() as usize).saturating_sub(text.as_ptr() as usize);
let off = at + rel;
let (name, _) = auth_param(e).map_err(|x| x.shifted(off))?;
count += 1;
if count > max {
return err(ErrorKind::TooManyMembers, off);
}
if names.repeats(text, rel, name, class::is_tchar) {
return err(ErrorKind::DuplicateParameter, off);
}
}
Ok(count)
}
pub fn parse_credentials<'a>(value: &'a [u8], limits: &Limits) -> Result<Credentials<'a>> {
let lead = skip_ows(value);
let v = trim_ows(value);
let n = v.iter().take_while(|&&b| class::is_tchar(b)).count();
if n == 0 {
return Err(refused(v, 0, ErrorKind::NotToken).shifted(lead));
}
let scheme = v.get(..n).unwrap_or(&[]);
let rest = v.get(n..).unwrap_or(&[]);
if rest.is_empty() {
return Ok(Credentials {
scheme,
payload: AuthPayload::None,
});
}
let spaces = rest.iter().take_while(|&&b| b == b' ').count();
if spaces == 0 {
return Err(refused(v, n, ErrorKind::NotToken).shifted(lead));
}
let at = n + spaces;
let body = v.get(at..).unwrap_or(&[]);
if is_token68(body) {
return Ok(Credentials {
scheme,
payload: AuthPayload::Token68(body),
});
}
let first = body
.get(
body.iter()
.take_while(|&&b| matches!(b, b',' | b' ' | b'\t'))
.count()..,
)
.unwrap_or(&[]);
if !is_param_start(first) && !first.is_empty() {
let t68 = body
.iter()
.take_while(|&&b| class::is_token68_char(b) || b == b'=')
.count();
return Err(refused(v, at + t68, ErrorKind::TrailingData).shifted(lead));
}
let count = check_params(body, lead + at, limits)?;
Ok(Credentials {
scheme,
payload: AuthPayload::Params(AuthParams { text: body, count }),
})
}
pub fn bearer(value: &[u8]) -> Result<Option<&[u8]>> {
let c = parse_credentials(value, &Limits::DEFAULT)?;
if !c.scheme_is("bearer") {
return Ok(None);
}
match c.payload {
AuthPayload::Token68(t) => Ok(Some(t)),
AuthPayload::None => err(ErrorKind::Empty, value.len()),
AuthPayload::Params(p) => {
let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
err(ErrorKind::InvalidByte, at)
}
}
}
pub fn basic(value: &[u8]) -> Result<Option<&[u8]>> {
let c = parse_credentials(value, &Limits::DEFAULT)?;
if !c.scheme_is("basic") {
return Ok(None);
}
let t = match c.payload {
AuthPayload::Token68(t) => t,
AuthPayload::None => return err(ErrorKind::Empty, value.len()),
AuthPayload::Params(p) => {
let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
return err(ErrorKind::InvalidBase64, at);
}
};
let at = (t.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
let body = t.iter().take_while(|&&b| class::is_base64_char(b)).count();
let pad = t.len() - body;
if body < t.len() && t.get(body) != Some(&b'=') || pad > 2 || t.len() % 4 != 0 {
return err(ErrorKind::InvalidBase64, at + body);
}
Ok(Some(t))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Challenge<'a> {
scheme: &'a [u8],
payload: AuthPayload<'a>,
}
impl<'a> Challenge<'a> {
#[must_use]
pub const fn scheme(&self) -> &'a [u8] {
self.scheme
}
#[must_use]
pub fn scheme_is(&self, s: &str) -> bool {
self.scheme.eq_ignore_ascii_case(s.as_bytes())
}
#[must_use]
pub const fn payload(&self) -> AuthPayload<'a> {
self.payload
}
#[must_use]
pub fn param(&self, name: &str) -> Option<Word<'a>> {
match self.payload {
AuthPayload::Params(p) => p.get(name),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Challenges<'a> {
list: list::List<'a>,
value: &'a [u8],
count: usize,
}
impl<'a> Challenges<'a> {
#[must_use]
pub fn iter(&self) -> ChallengeIter<'a> {
ChallengeIter {
value: self.value,
elements: self.list.iter().peekable(),
}
}
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
}
#[derive(Clone, Debug)]
pub struct ChallengeIter<'a> {
value: &'a [u8],
elements: Peekable<list::Elements<'a>>,
}
fn challenge_start(e: &[u8]) -> (&[u8], &[u8]) {
let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
let scheme = e.get(..n).unwrap_or(&[]);
let rest = e.get(n..).unwrap_or(&[]);
let rest = rest
.get(rest.iter().take_while(|&&b| b == b' ').count()..)
.unwrap_or(&[]);
(scheme, rest)
}
impl<'a> Iterator for ChallengeIter<'a> {
type Item = Challenge<'a>;
fn next(&mut self) -> Option<Challenge<'a>> {
let e = self.elements.next()?;
let (scheme, rest) = challenge_start(e);
let at = (e.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize);
let spaced_list = rest.is_empty()
&& self.value.get(at + scheme.len()) == Some(&b' ')
&& self.elements.peek().is_some_and(|x| is_param_start(x));
if rest.is_empty() && !spaced_list {
return Some(Challenge {
scheme,
payload: AuthPayload::None,
});
}
if !spaced_list && is_token68(rest) {
return Some(Challenge {
scheme,
payload: AuthPayload::Token68(rest),
});
}
let first = if spaced_list {
self.elements.peek().copied().unwrap_or(rest)
} else {
rest
};
let start = (first.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize);
let mut end = start + first.len();
let mut count = usize::from(!spaced_list);
while let Some(next) = self.elements.peek() {
if !is_param_start(next) {
break;
}
end =
(next.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize) + next.len();
count += 1;
let _skipped = self.elements.next();
}
let text = self.value.get(start..end).unwrap_or(&[]);
Some(Challenge {
scheme,
payload: AuthPayload::Params(AuthParams { text, count }),
})
}
}
pub fn parse_challenges<'a>(value: &'a [u8], limits: &Limits) -> Result<Challenges<'a>> {
let l = list::parse(value, limits)?;
let mut count = 0usize;
let mut elements = l.iter().peekable();
while let Some(e) = elements.next() {
let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
if is_param_start(e) {
return err(ErrorKind::ExpectedSeparator, at);
}
let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
if n == 0 {
return Err(refused(e, 0, ErrorKind::NotToken).shifted(at));
}
let (_, rest) = challenge_start(e);
if e.get(n).is_some_and(|&b| b != b' ') {
return Err(refused(e, n, ErrorKind::NotToken).shifted(at));
}
count += 1;
let spaced_list = rest.is_empty()
&& value.get(at + n) == Some(&b' ')
&& elements.peek().is_some_and(|x| is_param_start(x));
if !spaced_list && (rest.is_empty() || is_token68(rest)) {
continue;
}
if !spaced_list && !is_param_start(rest) {
let off = e.len() - rest.len();
return Err(refused(e, off, ErrorKind::TrailingData).shifted(at));
}
let start = if spaced_list {
elements.peek().map_or(at, |x| {
(x.as_ptr() as usize).saturating_sub(value.as_ptr() as usize)
})
} else {
at + (e.len() - rest.len())
};
let mut end = at + e.len();
while let Some(next) = elements.peek() {
if !is_param_start(next) {
break;
}
end = (next.as_ptr() as usize).saturating_sub(value.as_ptr() as usize) + next.len();
let _skipped = elements.next();
}
let _params = check_params(value.get(start..end).unwrap_or(&[]), start, limits)?;
}
Ok(Challenges {
list: l,
value,
count,
})
}
fn put_challenge(
scheme: &str,
params: &[(&str, &str)],
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
out.put(scheme.as_bytes())?;
for (i, (name, value)) in params.iter().enumerate() {
out.put(if i == 0 { b" " } else { b", " })?;
out.put(name.as_bytes())?;
out.put(b"=")?;
if name.eq_ignore_ascii_case("realm") {
write_quoted(value.as_bytes(), out)?;
} else {
write_word(value.as_bytes(), out)?;
}
}
Ok(())
}
pub fn write_challenge(
scheme: &str,
params: &[(&str, &str)],
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
if !is_token(scheme.as_bytes()) || params.iter().any(|(n, _)| !is_token(n.as_bytes())) {
return Err(WriteError::NotToken);
}
unique_names(params)?;
if params.len() > MAX_CHECKED_PARAMS {
return Err(WriteError::OutOfRange);
}
let mut count = CountSink::new();
put_challenge(scheme, params, &mut count)?;
reserve(out, count.count())?;
put_challenge(scheme, params, out)
}
pub fn write_challenge_token68(
scheme: &str,
token68: &[u8],
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
if !is_token(scheme.as_bytes()) {
return Err(WriteError::NotToken);
}
if !is_token68(token68) {
let at = token68
.iter()
.position(|&b| !(class::is_token68_char(b) || b == b'='))
.unwrap_or(0);
return Err(WriteError::forbidden(at));
}
reserve(out, scheme.len() + 1 + token68.len())?;
out.put(scheme.as_bytes())?;
out.put(b" ")?;
out.put(token68)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::{format, vec::Vec};
use super::*;
#[test]
fn test_bearer_cases() {
assert_eq!(bearer(b"Bearer abc").unwrap(), Some(&b"abc"[..]));
assert_eq!(bearer(b"bearer abc").unwrap(), Some(&b"abc"[..]));
assert_eq!(bearer(b"Bearer abc").unwrap(), Some(&b"abc"[..]));
assert_eq!(bearer(b"Bearer abc==").unwrap(), Some(&b"abc=="[..]));
assert_eq!(bearer(b"Bearer").unwrap_err().kind(), ErrorKind::Empty);
assert_eq!(
bearer(b"Bearer abc def").unwrap_err().kind(),
ErrorKind::TrailingData
);
assert_eq!(
bearer(b"Bearer a=b").unwrap_err().kind(),
ErrorKind::InvalidByte
);
assert_eq!(
bearer(b"Bearer\tabc").unwrap_err().kind(),
ErrorKind::NotToken
);
assert_eq!(bearer(b"").unwrap_err().kind(), ErrorKind::NotToken);
assert_eq!(bearer(b"Digest a=b").unwrap(), None);
}
#[test]
fn test_basic_cases() {
assert_eq!(
basic(b"Basic dXNlcjpwYXNz").unwrap(),
Some(&b"dXNlcjpwYXNz"[..])
);
assert_eq!(basic(b"Basic YQ==").unwrap(), Some(&b"YQ=="[..]));
assert_eq!(basic(b"Basic").unwrap_err().kind(), ErrorKind::Empty);
assert_eq!(
basic(b"Basic YQ").unwrap_err().kind(),
ErrorKind::InvalidBase64
);
assert_eq!(
basic(b"Basic a-b_").unwrap_err().kind(),
ErrorKind::InvalidBase64
);
assert_eq!(
basic(b"Basic YQ===").unwrap_err().kind(),
ErrorKind::InvalidBase64
);
assert_eq!(basic(b"Bearer x").unwrap(), None);
}
#[test]
fn test_credentials_params_and_duplicates() {
let l = Limits::DEFAULT;
let c = parse_credentials(b"Digest username = \"a\" , realm=r, ", &l).unwrap();
let AuthPayload::Params(p) = c.payload() else {
panic!("params")
};
assert_eq!(p.len(), 2);
assert!(p.get("USERNAME").unwrap().eq_bytes(b"a"));
assert_eq!(
parse_credentials(b"Digest a=b, A=c", &l)
.unwrap_err()
.kind(),
ErrorKind::DuplicateParameter
);
assert_eq!(
parse_credentials(b"Negotiate token68===", &l)
.unwrap()
.payload(),
AuthPayload::Token68(b"token68===")
);
assert_eq!(
parse_credentials(b"Digest a", &l).unwrap().payload(),
AuthPayload::Token68(b"a")
);
assert_eq!(
parse_credentials(b"Digest a b", &l).unwrap_err().kind(),
ErrorKind::TrailingData
);
assert_eq!(
parse_credentials(b"Digest a=", &l).unwrap().payload(),
AuthPayload::Token68(b"a=")
);
assert_eq!(
parse_credentials(b"Digest a=, b=1", &l).unwrap_err().kind(),
ErrorKind::MissingValue
);
assert_eq!(
parse_credentials(b"Digest a=\"x", &l).unwrap_err().kind(),
ErrorKind::UnterminatedQuote
);
}
#[test]
fn test_credentials_debug_redacted() {
let c = parse_credentials(b"Bearer s3cr3t-token", &Limits::DEFAULT).unwrap();
let d = format!("{c:?} {:?}", c.payload());
assert!(!d.contains("s3cr3t"));
let p = parse_credentials(b"Digest response=\"s3cr3t\"", &Limits::DEFAULT).unwrap();
let d = format!("{:?}", p.payload());
assert_eq!(d, "AuthParams([\"response\"])");
}
#[test]
fn test_challenges_rfc_examples() {
let l = Limits::DEFAULT;
let names = |v: &[u8]| -> Vec<Vec<u8>> {
parse_challenges(v, &l)
.unwrap()
.iter()
.map(|c| c.scheme().to_vec())
.collect()
};
assert_eq!(
names(b"Basic realm=\"a\", Bearer"),
[b"Basic".to_vec(), b"Bearer".to_vec()]
);
let c = parse_challenges(b"Bearer realm=\"a\", error=\"invalid_token\"", &l).unwrap();
let one: Vec<_> = c.iter().collect();
assert_eq!(one.len(), 1);
assert!(one[0].param("error").unwrap().eq_bytes(b"invalid_token"));
assert_eq!(
names(b"Negotiate abc==, Basic realm=x"),
[b"Negotiate".to_vec(), b"Basic".to_vec()]
);
assert_eq!(
names(b"A, , B, ,C"),
[b"A".to_vec(), b"B".to_vec(), b"C".to_vec()]
);
let t68 = parse_challenges(b"Negotiate abc==", &l).unwrap();
assert_eq!(
t68.iter().next().unwrap().payload(),
AuthPayload::Token68(b"abc==")
);
}
#[test]
fn test_challenge_refusals() {
let l = Limits::DEFAULT;
let k = |v: &[u8]| parse_challenges(v, &l).unwrap_err().kind();
assert_eq!(k(b"Basic realm=a, realm=b"), ErrorKind::DuplicateParameter);
assert_eq!(k(b"realm=a"), ErrorKind::ExpectedSeparator);
assert_eq!(k(b"Basic\trealm=a"), ErrorKind::NotToken);
assert_eq!(k(b"Basic abc def"), ErrorKind::TrailingData);
assert!(parse_challenges(b"Basic realm=a, Digest realm=b", &l).is_ok());
}
#[test]
fn test_challenge_writers_round_trip() {
let mut buf = [0u8; 128];
let mut out = crate::SliceSink::new(&mut buf);
write_challenge(
"Bearer",
&[("realm", "api"), ("scope", "read write")],
&mut out,
)
.unwrap();
let c = parse_challenges(out.written(), &Limits::DEFAULT).unwrap();
let first = c.iter().next().unwrap();
assert!(first.param("realm").unwrap().eq_bytes(b"api"));
assert!(first.param("scope").unwrap().eq_bytes(b"read write"));
assert_eq!(
write_challenge("Bad scheme", &[], &mut out),
Err(WriteError::NotToken)
);
assert_eq!(
write_challenge("Basic", &[("realm", "a"), ("Realm", "b")], &mut out),
Err(WriteError::DuplicateName)
);
assert_eq!(
write_challenge("Basic", &[("realm", "a\r\n")], &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
write_challenge_token68("Negotiate", b"a b", &mut out),
Err(WriteError::Forbidden { at: 1 })
);
}
}