Skip to main content

parse_net/
auth.rs

1//! `Authorization`, `Proxy-Authorization`, `WWW-Authenticate`, and
2//! `Proxy-Authenticate` (RFC 9110 section 11), with the `Bearer` (RFC 6750)
3//! and `Basic` (RFC 7617) payloads.
4//!
5//! Credentials are secrets: `Debug` on [`Credentials`] and [`AuthPayload`]
6//! prints the scheme and parameter names only.
7//!
8//! # Examples
9//!
10//! ```
11//! use parse_net::{self as pn, auth, Limits};
12//!
13//! assert_eq!(pn::bearer(b"bearer  mF_9.B5f-4.1JqM==")?, Some(&b"mF_9.B5f-4.1JqM=="[..]));
14//! assert_eq!(pn::bearer(b"Basic dXNlcjpwYXNz")?, None);
15//! assert_eq!(pn::basic(b"Basic dXNlcjpwYXNz")?, Some(&b"dXNlcjpwYXNz"[..]));
16//!
17//! let c = auth::parse_credentials(b"Digest username=\"a\", realm=\"r\"", &Limits::DEFAULT)?;
18//! assert!(c.scheme_is("digest"));
19//! assert_eq!(format!("{c:?}"), "Credentials { scheme: \"Digest\", .. }");
20//! # Ok::<(), pn::ParseError>(())
21//! ```
22
23use core::{fmt, iter::Peekable};
24
25use crate::{
26    class,
27    error::{ErrorKind, ParseError, Result, WriteError, err},
28    limits::Limits,
29    list,
30    param::{MAX_CHECKED_PARAMS, Names},
31    scan::control_kind,
32    text::{Word, is_token, parse_word, skip_ows, trim_ows},
33    write::{CountSink, Sink, reserve, unique_names, write_quoted, write_word},
34};
35
36/// Limits for walking text that was already validated.
37const RELAXED: Limits = Limits::DEFAULT
38    .max_list_members(u16::MAX)
39    .max_empty_elements(u16::MAX);
40
41/// Whether `s` is a `token68`: `1*( ALPHA / DIGIT / "-" / "." / "_" / "~" /
42/// "+" / "/" ) *"="`.
43fn is_token68(s: &[u8]) -> bool {
44    let body = s.iter().take_while(|&&b| class::is_token68_char(b)).count();
45    body > 0
46        && s.get(body..)
47            .is_some_and(|pad| pad.iter().all(|&b| b == b'='))
48}
49
50/// One `auth-param`: `token BWS "=" BWS ( token / quoted-string )`.
51fn auth_param(e: &[u8]) -> Result<(&[u8], Word<'_>)> {
52    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
53    if n == 0 {
54        return Err(refused(e, 0, ErrorKind::NotToken));
55    }
56    let name = e.get(..n).unwrap_or(&[]);
57    let mut i = n + skip_ows(e.get(n..).unwrap_or(&[]));
58    if e.get(i) != Some(&b'=') {
59        return Err(refused(e, i, ErrorKind::MissingValue));
60    }
61    i += 1;
62    i += skip_ows(e.get(i..).unwrap_or(&[]));
63    let (w, used) = parse_word(e.get(i..).unwrap_or(&[])).map_err(|x| match x.kind() {
64        ErrorKind::Empty => refused(e, i, ErrorKind::MissingValue),
65        _ => x.shifted(i),
66    })?;
67    if i + used != e.len() {
68        return Err(refused(e, i + used, ErrorKind::TrailingData));
69    }
70    Ok((name, w))
71}
72
73fn refused(e: &[u8], at: usize, kind: ErrorKind) -> ParseError {
74    match e.get(at) {
75        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
76        _ => ParseError::new(kind, at),
77    }
78}
79
80/// Whether element `e` is an `auth-param` (a token, optional whitespace,
81/// then `=`), as opposed to the start of a challenge.
82fn is_param_start(e: &[u8]) -> bool {
83    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
84    n > 0 && e.get(n + skip_ows(e.get(n..).unwrap_or(&[]))) == Some(&b'=')
85}
86
87/// Comma-separated `auth-param`s, validated.
88///
89/// `Debug` prints the parameter names only: values can be secrets.
90///
91/// # Examples
92///
93/// ```
94/// use parse_net::{auth, AuthPayload, Limits};
95///
96/// let c = auth::parse_credentials(b"Digest username=\"a\", realm=\"r\"", &Limits::DEFAULT)?;
97/// let AuthPayload::Params(p) = c.payload() else { unreachable!() };
98/// assert_eq!(p.len(), 2);
99/// assert!(p.get("realm").is_some_and(|v| v.eq_bytes(b"r")));
100/// # Ok::<(), parse_net::ParseError>(())
101/// ```
102#[derive(Clone, Copy, PartialEq, Eq, Hash)]
103pub struct AuthParams<'a> {
104    text: &'a [u8],
105    count: usize,
106}
107
108impl<'a> AuthParams<'a> {
109    /// The parameters, in order.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use parse_net::{auth, AuthPayload, Limits};
115    ///
116    /// let c = auth::parse_credentials(b"Digest username=\"a\", nc=1", &Limits::DEFAULT)?;
117    /// let AuthPayload::Params(p) = c.payload() else { unreachable!() };
118    /// let names: Vec<&[u8]> = p.iter().map(|(n, _)| n).collect();
119    /// assert_eq!(names, [&b"username"[..], b"nc"]);
120    /// # Ok::<(), parse_net::ParseError>(())
121    /// ```
122    pub fn iter(&self) -> impl Iterator<Item = (&'a [u8], Word<'a>)> + use<'a> {
123        list::parse(self.text, &RELAXED)
124            .into_iter()
125            .flat_map(|l| l.into_iter())
126            .filter_map(|e| auth_param(e).ok())
127    }
128
129    /// The value of the parameter `name` (ASCII case-insensitive).
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use parse_net::{auth, AuthPayload, Limits};
135    ///
136    /// let c = auth::parse_credentials(b"Digest Realm=\"api\"", &Limits::DEFAULT)?;
137    /// let AuthPayload::Params(p) = c.payload() else { unreachable!() };
138    /// assert!(p.get("realm").is_some_and(|v| v.eq_bytes(b"api")));
139    /// assert!(p.get("nonce").is_none());
140    /// # Ok::<(), parse_net::ParseError>(())
141    /// ```
142    #[must_use]
143    pub fn get(&self, name: &str) -> Option<Word<'a>> {
144        self.iter()
145            .find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
146            .map(|(_, v)| v)
147    }
148
149    /// The number of parameters.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use parse_net::{auth, AuthPayload, Limits};
155    ///
156    /// let c = auth::parse_credentials(b"Digest a=1, b=2, c=3", &Limits::DEFAULT)?;
157    /// let AuthPayload::Params(p) = c.payload() else { unreachable!() };
158    /// assert_eq!(p.len(), 3);
159    /// # Ok::<(), parse_net::ParseError>(())
160    /// ```
161    #[must_use]
162    pub const fn len(&self) -> usize {
163        self.count
164    }
165
166    /// Whether there are no parameters.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use parse_net::{auth, AuthPayload, Limits};
172    ///
173    /// let c = auth::parse_credentials(b"Digest a=1", &Limits::DEFAULT)?;
174    /// let AuthPayload::Params(p) = c.payload() else { unreachable!() };
175    /// assert!(!p.is_empty());
176    /// # Ok::<(), parse_net::ParseError>(())
177    /// ```
178    #[must_use]
179    pub const fn is_empty(&self) -> bool {
180        self.count == 0
181    }
182}
183
184impl fmt::Debug for AuthParams<'_> {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.write_str("AuthParams(")?;
187        f.debug_list()
188            .entries(
189                self.iter()
190                    .map(|(n, _)| core::str::from_utf8(n).unwrap_or("?")),
191            )
192            .finish()?;
193        f.write_str(")")
194    }
195}
196
197/// What follows the scheme of a credential or a challenge.
198///
199/// # Examples
200///
201/// ```
202/// use parse_net::{auth, AuthPayload, Limits};
203///
204/// let l = Limits::DEFAULT;
205/// assert_eq!(auth::parse_credentials(b"Negotiate", &l)?.payload(), AuthPayload::None);
206/// assert_eq!(auth::parse_credentials(b"Bearer abc123", &l)?.payload(), AuthPayload::Token68(b"abc123"));
207/// assert!(matches!(auth::parse_credentials(b"Digest a=b", &l)?.payload(), AuthPayload::Params(_)));
208/// # Ok::<(), parse_net::ParseError>(())
209/// ```
210#[derive(Clone, Copy, PartialEq, Eq, Hash)]
211pub enum AuthPayload<'a> {
212    /// Nothing.
213    None,
214    /// A `token68`, such as a bearer token or `Basic` base64 text.
215    Token68(&'a [u8]),
216    /// `auth-param`s.
217    Params(AuthParams<'a>),
218}
219
220impl fmt::Debug for AuthPayload<'_> {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        match self {
223            AuthPayload::None => f.write_str("None"),
224            AuthPayload::Token68(_) => f.write_str("Token68(..)"),
225            AuthPayload::Params(p) => fmt::Debug::fmt(p, f),
226        }
227    }
228}
229
230/// Validated credentials (`Authorization`, `Proxy-Authorization`).
231///
232/// `Debug` prints the scheme only, and not even that with no payload: a bare
233/// value may be a key sent without a scheme.
234///
235/// # Examples
236///
237/// ```
238/// use parse_net::{auth, AuthPayload, Limits};
239///
240/// let c = auth::parse_credentials(b"Bearer abc123", &Limits::DEFAULT)?;
241/// assert!(c.scheme_is("bearer"));
242/// assert_eq!(c.payload(), AuthPayload::Token68(b"abc123"));
243/// # Ok::<(), parse_net::ParseError>(())
244/// ```
245#[derive(Clone, Copy, PartialEq, Eq, Hash)]
246pub struct Credentials<'a> {
247    scheme: &'a [u8],
248    payload: AuthPayload<'a>,
249}
250
251impl<'a> Credentials<'a> {
252    /// The scheme, as sent.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use parse_net::{auth, Limits};
258    ///
259    /// let c = auth::parse_credentials(b"Bearer abc123", &Limits::DEFAULT)?;
260    /// assert_eq!(c.scheme(), b"Bearer");
261    /// # Ok::<(), parse_net::ParseError>(())
262    /// ```
263    #[must_use]
264    pub const fn scheme(&self) -> &'a [u8] {
265        self.scheme
266    }
267
268    /// Whether the scheme is `s` (ASCII case-insensitive).
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use parse_net::{auth, Limits};
274    ///
275    /// let c = auth::parse_credentials(b"BASIC dXNlcjpwYXNz", &Limits::DEFAULT)?;
276    /// assert!(c.scheme_is("basic") && !c.scheme_is("bearer"));
277    /// # Ok::<(), parse_net::ParseError>(())
278    /// ```
279    #[must_use]
280    pub fn scheme_is(&self, s: &str) -> bool {
281        self.scheme.eq_ignore_ascii_case(s.as_bytes())
282    }
283
284    /// The payload.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use parse_net::{auth, AuthPayload, Limits};
290    ///
291    /// let c = auth::parse_credentials(b"Basic dXNlcjpwYXNz", &Limits::DEFAULT)?;
292    /// assert_eq!(c.payload(), AuthPayload::Token68(b"dXNlcjpwYXNz"));
293    /// # Ok::<(), parse_net::ParseError>(())
294    /// ```
295    #[must_use]
296    pub const fn payload(&self) -> AuthPayload<'a> {
297        self.payload
298    }
299}
300
301impl fmt::Debug for Credentials<'_> {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        // A value with no payload may be a key sent without a scheme.
304        if self.payload == AuthPayload::None {
305            return f.debug_struct("Credentials").finish_non_exhaustive();
306        }
307        f.debug_struct("Credentials")
308            .field("scheme", &core::str::from_utf8(self.scheme).unwrap_or("?"))
309            .finish_non_exhaustive()
310    }
311}
312
313/// Validates the `auth-param` list `text` (from `at` in the field) with no
314/// repeated name, and counts it.
315fn check_params(text: &[u8], at: usize, limits: &Limits) -> Result<usize> {
316    let l = list::parse(text, limits).map_err(|e| e.shifted(at))?;
317    let max = usize::from(limits.params).min(MAX_CHECKED_PARAMS);
318    let (mut count, mut names) = (0usize, Names::new());
319    for e in l.iter() {
320        let rel = (e.as_ptr() as usize).saturating_sub(text.as_ptr() as usize);
321        let off = at + rel;
322        let (name, _) = auth_param(e).map_err(|x| x.shifted(off))?;
323        count += 1;
324        if count > max {
325            return err(ErrorKind::TooManyMembers, off);
326        }
327        // A repeated name within one credential or challenge is refused
328        // (RFC 9110 section 11.2); the name starts the element.
329        if names.repeats(text, rel, name, class::is_tchar) {
330            return err(ErrorKind::DuplicateParameter, off);
331        }
332    }
333    Ok(count)
334}
335
336/// Parses `credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]`.
337///
338/// One credential per field; parameter names may not repeat; OWS around
339/// `=` in a parameter is accepted (BWS).
340///
341/// # Errors
342///
343/// [`ErrorKind::NotToken`] for the scheme or a parameter name;
344/// [`ErrorKind::DuplicateParameter`]; [`ErrorKind::MissingValue`];
345/// [`ErrorKind::TrailingData`] (for example a second credential); the
346/// quoted-string and list errors.
347///
348/// # Examples
349///
350/// ```
351/// use parse_net::{auth, Limits};
352///
353/// assert!(auth::parse_credentials(b"Digest a=b, a=c", &Limits::DEFAULT).is_err());
354/// ```
355pub fn parse_credentials<'a>(value: &'a [u8], limits: &Limits) -> Result<Credentials<'a>> {
356    let lead = skip_ows(value);
357    let v = trim_ows(value);
358    let n = v.iter().take_while(|&&b| class::is_tchar(b)).count();
359    if n == 0 {
360        return Err(refused(v, 0, ErrorKind::NotToken).shifted(lead));
361    }
362    let scheme = v.get(..n).unwrap_or(&[]);
363    let rest = v.get(n..).unwrap_or(&[]);
364    if rest.is_empty() {
365        return Ok(Credentials {
366            scheme,
367            payload: AuthPayload::None,
368        });
369    }
370    let spaces = rest.iter().take_while(|&&b| b == b' ').count();
371    if spaces == 0 {
372        return Err(refused(v, n, ErrorKind::NotToken).shifted(lead));
373    }
374    let at = n + spaces;
375    let body = v.get(at..).unwrap_or(&[]);
376    if is_token68(body) {
377        return Ok(Credentials {
378            scheme,
379            payload: AuthPayload::Token68(body),
380        });
381    }
382    // Recipients ignore empty list elements (RFC 9110 section 5.6.1.2), the
383    // leading ones included.
384    let first = body
385        .get(
386            body.iter()
387                .take_while(|&&b| matches!(b, b',' | b' ' | b'\t'))
388                .count()..,
389        )
390        .unwrap_or(&[]);
391    if !is_param_start(first) && !first.is_empty() {
392        // A token68 followed by more, or a second scheme.
393        let t68 = body
394            .iter()
395            .take_while(|&&b| class::is_token68_char(b) || b == b'=')
396            .count();
397        return Err(refused(v, at + t68, ErrorKind::TrailingData).shifted(lead));
398    }
399    let count = check_params(body, lead + at, limits)?;
400    Ok(Credentials {
401        scheme,
402        payload: AuthPayload::Params(AuthParams { text: body, count }),
403    })
404}
405
406/// The `Bearer` token of an `Authorization` value (RFC 6750 section 2.1):
407/// `Some(token)` when the scheme is `Bearer` (case-insensitive), `None` for
408/// another scheme.
409///
410/// # Errors
411///
412/// [`ErrorKind::Empty`] for `Bearer` alone; [`ErrorKind::InvalidByte`] when
413/// the payload is not a `b64token`; [`ErrorKind::TrailingData`]; the scheme
414/// errors.
415///
416/// # Examples
417///
418/// ```
419/// assert_eq!(parse_net::bearer(b"Bearer abc123")?, Some(&b"abc123"[..]));
420/// assert_eq!(parse_net::bearer(b"Basic dXNlcjpwYXNz")?, None);
421/// assert!(parse_net::bearer(b"Bearer").is_err());
422/// # Ok::<(), parse_net::ParseError>(())
423/// ```
424pub fn bearer(value: &[u8]) -> Result<Option<&[u8]>> {
425    let c = parse_credentials(value, &Limits::DEFAULT)?;
426    if !c.scheme_is("bearer") {
427        return Ok(None);
428    }
429    match c.payload {
430        AuthPayload::Token68(t) => Ok(Some(t)),
431        AuthPayload::None => err(ErrorKind::Empty, value.len()),
432        AuthPayload::Params(p) => {
433            let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
434            err(ErrorKind::InvalidByte, at)
435        }
436    }
437}
438
439/// The base64 text of a `Basic` `Authorization` value (RFC 7617): `Some`
440/// when the scheme is `Basic`, `None` for another scheme. The text is
441/// checked (the base64 alphabet, padding only at the end, a length that is a
442/// multiple of four) and not decoded.
443///
444/// # Errors
445///
446/// [`ErrorKind::InvalidBase64`]; [`ErrorKind::Empty`] for `Basic` alone;
447/// the scheme errors.
448///
449/// # Examples
450///
451/// ```
452/// assert_eq!(parse_net::basic(b"Basic dXNlcjpwYXNz")?, Some(&b"dXNlcjpwYXNz"[..]));
453/// assert_eq!(parse_net::basic(b"Bearer abc123")?, None);
454/// assert!(parse_net::basic(b"Basic YQ").is_err());
455/// # Ok::<(), parse_net::ParseError>(())
456/// ```
457pub fn basic(value: &[u8]) -> Result<Option<&[u8]>> {
458    let c = parse_credentials(value, &Limits::DEFAULT)?;
459    if !c.scheme_is("basic") {
460        return Ok(None);
461    }
462    let t = match c.payload {
463        AuthPayload::Token68(t) => t,
464        AuthPayload::None => return err(ErrorKind::Empty, value.len()),
465        AuthPayload::Params(p) => {
466            let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
467            return err(ErrorKind::InvalidBase64, at);
468        }
469    };
470    let at = (t.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
471    let body = t.iter().take_while(|&&b| class::is_base64_char(b)).count();
472    let pad = t.len() - body;
473    if body < t.len() && t.get(body) != Some(&b'=') || pad > 2 || t.len() % 4 != 0 {
474        return err(ErrorKind::InvalidBase64, at + body);
475    }
476    Ok(Some(t))
477}
478
479/// One challenge.
480///
481/// # Examples
482///
483/// ```
484/// use parse_net::{auth, Limits};
485///
486/// let c = auth::parse_challenges(b"Bearer realm=\"api\", error=invalid_token", &Limits::DEFAULT)?;
487/// let first = c.iter().next().expect("one challenge");
488/// assert!(first.scheme_is("bearer"));
489/// assert!(first.param("error").is_some_and(|v| v.eq_bytes(b"invalid_token")));
490/// # Ok::<(), parse_net::ParseError>(())
491/// ```
492#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
493pub struct Challenge<'a> {
494    scheme: &'a [u8],
495    payload: AuthPayload<'a>,
496}
497
498impl<'a> Challenge<'a> {
499    /// The scheme, as sent.
500    ///
501    /// # Examples
502    ///
503    /// ```
504    /// use parse_net::{auth, Limits};
505    ///
506    /// let c = auth::parse_challenges(b"Basic realm=\"x\", Negotiate", &Limits::DEFAULT)?;
507    /// let schemes: Vec<&[u8]> = c.iter().map(|x| x.scheme()).collect();
508    /// assert_eq!(schemes, [&b"Basic"[..], b"Negotiate"]);
509    /// # Ok::<(), parse_net::ParseError>(())
510    /// ```
511    #[must_use]
512    pub const fn scheme(&self) -> &'a [u8] {
513        self.scheme
514    }
515
516    /// Whether the scheme is `s` (ASCII case-insensitive).
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// use parse_net::{auth, Limits};
522    ///
523    /// let c = auth::parse_challenges(b"NEGOTIATE", &Limits::DEFAULT)?;
524    /// assert!(c.iter().next().is_some_and(|x| x.scheme_is("negotiate")));
525    /// # Ok::<(), parse_net::ParseError>(())
526    /// ```
527    #[must_use]
528    pub fn scheme_is(&self, s: &str) -> bool {
529        self.scheme.eq_ignore_ascii_case(s.as_bytes())
530    }
531
532    /// The payload.
533    ///
534    /// # Examples
535    ///
536    /// ```
537    /// use parse_net::{auth, AuthPayload, Limits};
538    ///
539    /// let c = auth::parse_challenges(b"Negotiate abc==", &Limits::DEFAULT)?;
540    /// let first = c.iter().next().expect("one challenge");
541    /// assert_eq!(first.payload(), AuthPayload::Token68(b"abc=="));
542    /// # Ok::<(), parse_net::ParseError>(())
543    /// ```
544    #[must_use]
545    pub const fn payload(&self) -> AuthPayload<'a> {
546        self.payload
547    }
548
549    /// The parameter `name` (`realm`, `error`, `scope`, ...).
550    ///
551    /// # Examples
552    ///
553    /// ```
554    /// use parse_net::{auth, Limits};
555    ///
556    /// let c = auth::parse_challenges(b"Basic realm=\"simple\", charset=UTF-8", &Limits::DEFAULT)?;
557    /// let first = c.iter().next().expect("one challenge");
558    /// assert!(first.param("Realm").is_some_and(|v| v.eq_bytes(b"simple")));
559    /// assert!(first.param("scope").is_none());
560    /// # Ok::<(), parse_net::ParseError>(())
561    /// ```
562    #[must_use]
563    pub fn param(&self, name: &str) -> Option<Word<'a>> {
564        match self.payload {
565            AuthPayload::Params(p) => p.get(name),
566            _ => None,
567        }
568    }
569}
570
571/// Validated challenges.
572///
573/// # Examples
574///
575/// ```
576/// use parse_net::{auth, Limits};
577///
578/// let c = auth::parse_challenges(b"Basic realm=\"a\", Bearer", &Limits::DEFAULT)?;
579/// assert_eq!(c.len(), 2);
580/// assert!(c.iter().any(|x| x.scheme_is("bearer")));
581/// # Ok::<(), parse_net::ParseError>(())
582/// ```
583#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
584pub struct Challenges<'a> {
585    list: list::List<'a>,
586    value: &'a [u8],
587    count: usize,
588}
589
590impl<'a> Challenges<'a> {
591    /// The challenges, in order.
592    ///
593    /// # Examples
594    ///
595    /// ```
596    /// use parse_net::{auth, Limits};
597    ///
598    /// let c = auth::parse_challenges(b"Basic realm=\"a\", Bearer", &Limits::DEFAULT)?;
599    /// let mut it = c.iter();
600    /// assert!(it.next().is_some_and(|x| x.scheme_is("basic")));
601    /// assert!(it.next().is_some_and(|x| x.scheme_is("bearer")));
602    /// assert!(it.next().is_none());
603    /// # Ok::<(), parse_net::ParseError>(())
604    /// ```
605    #[must_use]
606    pub fn iter(&self) -> ChallengeIter<'a> {
607        ChallengeIter {
608            value: self.value,
609            elements: self.list.iter().peekable(),
610        }
611    }
612
613    /// The number of challenges.
614    ///
615    /// # Examples
616    ///
617    /// ```
618    /// use parse_net::{auth, Limits};
619    ///
620    /// let c = auth::parse_challenges(b"A, B realm=x, C abc==", &Limits::DEFAULT)?;
621    /// assert_eq!(c.len(), 3);
622    /// # Ok::<(), parse_net::ParseError>(())
623    /// ```
624    #[must_use]
625    pub const fn len(&self) -> usize {
626        self.count
627    }
628
629    /// Whether there are no challenges.
630    ///
631    /// # Examples
632    ///
633    /// ```
634    /// use parse_net::{auth, Limits};
635    ///
636    /// assert!(auth::parse_challenges(b"", &Limits::DEFAULT)?.is_empty());
637    /// assert!(!auth::parse_challenges(b"Basic", &Limits::DEFAULT)?.is_empty());
638    /// # Ok::<(), parse_net::ParseError>(())
639    /// ```
640    #[must_use]
641    pub const fn is_empty(&self) -> bool {
642        self.count == 0
643    }
644}
645
646/// The challenges of a validated value.
647///
648/// # Examples
649///
650/// ```
651/// use parse_net::{auth::{self, ChallengeIter}, Limits};
652///
653/// let c = auth::parse_challenges(b"Negotiate, Basic realm=\"x\"", &Limits::DEFAULT)?;
654/// let it: ChallengeIter<'_> = c.iter();
655/// assert_eq!(it.count(), 2);
656/// # Ok::<(), parse_net::ParseError>(())
657/// ```
658#[derive(Clone, Debug)]
659pub struct ChallengeIter<'a> {
660    value: &'a [u8],
661    elements: Peekable<list::Elements<'a>>,
662}
663
664/// The start of challenge element `e`: its scheme and what follows.
665fn challenge_start(e: &[u8]) -> (&[u8], &[u8]) {
666    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
667    let scheme = e.get(..n).unwrap_or(&[]);
668    let rest = e.get(n..).unwrap_or(&[]);
669    let rest = rest
670        .get(rest.iter().take_while(|&&b| b == b' ').count()..)
671        .unwrap_or(&[]);
672    (scheme, rest)
673}
674
675impl<'a> Iterator for ChallengeIter<'a> {
676    type Item = Challenge<'a>;
677
678    fn next(&mut self) -> Option<Challenge<'a>> {
679        let e = self.elements.next()?;
680        let (scheme, rest) = challenge_start(e);
681        let at = (e.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize);
682        let spaced_list = rest.is_empty()
683            && self.value.get(at + scheme.len()) == Some(&b' ')
684            && self.elements.peek().is_some_and(|x| is_param_start(x));
685        if rest.is_empty() && !spaced_list {
686            return Some(Challenge {
687                scheme,
688                payload: AuthPayload::None,
689            });
690        }
691        if !spaced_list && is_token68(rest) {
692            return Some(Challenge {
693                scheme,
694                payload: AuthPayload::Token68(rest),
695            });
696        }
697        // The first parameter is in this element; the rest follow it as
698        // separate elements until the next challenge starts.
699        let first = if spaced_list {
700            self.elements.peek().copied().unwrap_or(rest)
701        } else {
702            rest
703        };
704        let start = (first.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize);
705        let mut end = start + first.len();
706        let mut count = usize::from(!spaced_list);
707        while let Some(next) = self.elements.peek() {
708            if !is_param_start(next) {
709                break;
710            }
711            end =
712                (next.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize) + next.len();
713            count += 1;
714            let _skipped = self.elements.next();
715        }
716        let text = self.value.get(start..end).unwrap_or(&[]);
717        Some(Challenge {
718            scheme,
719            payload: AuthPayload::Params(AuthParams { text, count }),
720        })
721    }
722}
723
724/// Parses `WWW-Authenticate` or `Proxy-Authenticate`: `#challenge`.
725///
726/// A challenge's own parameters are comma-separated too, so where one
727/// challenge ends is decided element by element: a token followed by `=` is
728/// a parameter of the current challenge; a token alone, or a token and
729/// `1*SP` before a `token68` or a parameter, starts a new one. A parameter
730/// name may not repeat within one challenge (RFC 9110 section 11.2).
731///
732/// # Errors
733///
734/// [`ErrorKind::DuplicateParameter`]; [`ErrorKind::NotToken`];
735/// [`ErrorKind::ExpectedSeparator`] for a parameter before any challenge;
736/// [`ErrorKind::TrailingData`]; the list and quoted-string errors.
737///
738/// # Examples
739///
740/// ```
741/// use parse_net::{auth, Limits};
742///
743/// let c = auth::parse_challenges(
744///     b"Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"",
745///     &Limits::DEFAULT)?;
746/// let all: Vec<_> = c.iter().collect();
747/// assert_eq!(all.len(), 2);
748/// assert!(all[0].param("type").is_some_and(|v| v.eq_bytes(b"1")));
749/// assert!(all[1].scheme_is("basic"));
750/// # Ok::<(), parse_net::ParseError>(())
751/// ```
752pub fn parse_challenges<'a>(value: &'a [u8], limits: &Limits) -> Result<Challenges<'a>> {
753    let l = list::parse(value, limits)?;
754    let mut count = 0usize;
755    let mut elements = l.iter().peekable();
756    while let Some(e) = elements.next() {
757        let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
758        if is_param_start(e) {
759            return err(ErrorKind::ExpectedSeparator, at);
760        }
761        let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
762        if n == 0 {
763            return Err(refused(e, 0, ErrorKind::NotToken).shifted(at));
764        }
765        let (_, rest) = challenge_start(e);
766        if e.get(n).is_some_and(|&b| b != b' ') {
767            return Err(refused(e, n, ErrorKind::NotToken).shifted(at));
768        }
769        count += 1;
770        // `Digest , realm=x`: a space, then a list with an empty first element.
771        let spaced_list = rest.is_empty()
772            && value.get(at + n) == Some(&b' ')
773            && elements.peek().is_some_and(|x| is_param_start(x));
774        if !spaced_list && (rest.is_empty() || is_token68(rest)) {
775            continue;
776        }
777        if !spaced_list && !is_param_start(rest) {
778            let off = e.len() - rest.len();
779            return Err(refused(e, off, ErrorKind::TrailingData).shifted(at));
780        }
781        // This challenge's parameters: the rest of this element and every
782        // following parameter element.
783        let start = if spaced_list {
784            elements.peek().map_or(at, |x| {
785                (x.as_ptr() as usize).saturating_sub(value.as_ptr() as usize)
786            })
787        } else {
788            at + (e.len() - rest.len())
789        };
790        let mut end = at + e.len();
791        while let Some(next) = elements.peek() {
792            if !is_param_start(next) {
793                break;
794            }
795            end = (next.as_ptr() as usize).saturating_sub(value.as_ptr() as usize) + next.len();
796            let _skipped = elements.next();
797        }
798        let _params = check_params(value.get(start..end).unwrap_or(&[]), start, limits)?;
799    }
800    Ok(Challenges {
801        list: l,
802        value,
803        count,
804    })
805}
806
807fn put_challenge(
808    scheme: &str,
809    params: &[(&str, &str)],
810    out: &mut impl Sink,
811) -> core::result::Result<(), WriteError> {
812    out.put(scheme.as_bytes())?;
813    for (i, (name, value)) in params.iter().enumerate() {
814        out.put(if i == 0 { b" " } else { b", " })?;
815        out.put(name.as_bytes())?;
816        out.put(b"=")?;
817        if name.eq_ignore_ascii_case("realm") {
818            write_quoted(value.as_bytes(), out)?;
819        } else {
820            write_word(value.as_bytes(), out)?;
821        }
822    }
823    Ok(())
824}
825
826/// Writes a challenge with parameters: `Bearer realm="api", error="invalid_token"`.
827/// `realm` is always quoted (RFC 9110 section 11.5); other values are tokens
828/// when they can be.
829///
830/// # Errors
831///
832/// [`WriteError::NotToken`] for the scheme or a name;
833/// [`WriteError::DuplicateName`] for a name given twice (RFC 9110 section
834/// 11.2 allows each once); [`WriteError::Forbidden`] for a control byte in a
835/// value; [`WriteError::Full`] (nothing written when the sink reports its
836/// room).
837///
838/// # Examples
839///
840/// ```
841/// use parse_net::{auth, SliceSink};
842///
843/// let mut buf = [0u8; 64];
844/// let mut out = SliceSink::new(&mut buf);
845/// auth::write_challenge("Bearer", &[("realm", "api"), ("error", "invalid_token")], &mut out).expect("room");
846/// assert_eq!(out.written(), b"Bearer realm=\"api\", error=invalid_token");
847/// ```
848pub fn write_challenge(
849    scheme: &str,
850    params: &[(&str, &str)],
851    out: &mut impl Sink,
852) -> core::result::Result<(), WriteError> {
853    if !is_token(scheme.as_bytes()) || params.iter().any(|(n, _)| !is_token(n.as_bytes())) {
854        return Err(WriteError::NotToken);
855    }
856    unique_names(params)?;
857    // What the parser reads (D-072): a longer list could not be read back.
858    if params.len() > MAX_CHECKED_PARAMS {
859        return Err(WriteError::OutOfRange);
860    }
861    let mut count = CountSink::new();
862    put_challenge(scheme, params, &mut count)?;
863    reserve(out, count.count())?;
864    put_challenge(scheme, params, out)
865}
866
867/// Writes a challenge with a `token68`: `Negotiate <token68>`.
868///
869/// # Errors
870///
871/// [`WriteError::NotToken`] for the scheme; [`WriteError::Forbidden`] when
872/// `token68` is not a `token68`; [`WriteError::Full`].
873///
874/// # Examples
875///
876/// ```
877/// use parse_net::{auth, SliceSink};
878///
879/// let mut buf = [0u8; 32];
880/// let mut out = SliceSink::new(&mut buf);
881/// auth::write_challenge_token68("Negotiate", b"YIIB==", &mut out).expect("room");
882/// assert_eq!(out.written(), b"Negotiate YIIB==");
883/// ```
884pub fn write_challenge_token68(
885    scheme: &str,
886    token68: &[u8],
887    out: &mut impl Sink,
888) -> core::result::Result<(), WriteError> {
889    if !is_token(scheme.as_bytes()) {
890        return Err(WriteError::NotToken);
891    }
892    if !is_token68(token68) {
893        let at = token68
894            .iter()
895            .position(|&b| !(class::is_token68_char(b) || b == b'='))
896            .unwrap_or(0);
897        return Err(WriteError::forbidden(at));
898    }
899    reserve(out, scheme.len() + 1 + token68.len())?;
900    out.put(scheme.as_bytes())?;
901    out.put(b" ")?;
902    out.put(token68)?;
903    Ok(())
904}
905
906#[cfg(test)]
907mod tests {
908    use std::{format, vec::Vec};
909
910    use super::*;
911
912    #[test]
913    fn test_bearer_cases() {
914        assert_eq!(bearer(b"Bearer abc").unwrap(), Some(&b"abc"[..]));
915        assert_eq!(bearer(b"bearer abc").unwrap(), Some(&b"abc"[..]));
916        assert_eq!(bearer(b"Bearer  abc").unwrap(), Some(&b"abc"[..]));
917        assert_eq!(bearer(b"Bearer abc==").unwrap(), Some(&b"abc=="[..]));
918        assert_eq!(bearer(b"Bearer").unwrap_err().kind(), ErrorKind::Empty);
919        assert_eq!(
920            bearer(b"Bearer abc def").unwrap_err().kind(),
921            ErrorKind::TrailingData
922        );
923        assert_eq!(
924            bearer(b"Bearer a=b").unwrap_err().kind(),
925            ErrorKind::InvalidByte
926        );
927        assert_eq!(
928            bearer(b"Bearer\tabc").unwrap_err().kind(),
929            ErrorKind::NotToken
930        );
931        assert_eq!(bearer(b"").unwrap_err().kind(), ErrorKind::NotToken);
932        assert_eq!(bearer(b"Digest a=b").unwrap(), None);
933    }
934
935    #[test]
936    fn test_basic_cases() {
937        assert_eq!(
938            basic(b"Basic dXNlcjpwYXNz").unwrap(),
939            Some(&b"dXNlcjpwYXNz"[..])
940        );
941        assert_eq!(basic(b"Basic YQ==").unwrap(), Some(&b"YQ=="[..]));
942        assert_eq!(basic(b"Basic").unwrap_err().kind(), ErrorKind::Empty);
943        assert_eq!(
944            basic(b"Basic YQ").unwrap_err().kind(),
945            ErrorKind::InvalidBase64
946        );
947        assert_eq!(
948            basic(b"Basic a-b_").unwrap_err().kind(),
949            ErrorKind::InvalidBase64
950        );
951        assert_eq!(
952            basic(b"Basic YQ===").unwrap_err().kind(),
953            ErrorKind::InvalidBase64
954        );
955        assert_eq!(basic(b"Bearer x").unwrap(), None);
956    }
957
958    #[test]
959    fn test_credentials_params_and_duplicates() {
960        let l = Limits::DEFAULT;
961        let c = parse_credentials(b"Digest username = \"a\" , realm=r, ", &l).unwrap();
962        let AuthPayload::Params(p) = c.payload() else {
963            panic!("params")
964        };
965        assert_eq!(p.len(), 2);
966        assert!(p.get("USERNAME").unwrap().eq_bytes(b"a"));
967        assert_eq!(
968            parse_credentials(b"Digest a=b, A=c", &l)
969                .unwrap_err()
970                .kind(),
971            ErrorKind::DuplicateParameter
972        );
973        assert_eq!(
974            parse_credentials(b"Negotiate token68===", &l)
975                .unwrap()
976                .payload(),
977            AuthPayload::Token68(b"token68===")
978        );
979        assert_eq!(
980            parse_credentials(b"Digest a", &l).unwrap().payload(),
981            AuthPayload::Token68(b"a")
982        );
983        assert_eq!(
984            parse_credentials(b"Digest a b", &l).unwrap_err().kind(),
985            ErrorKind::TrailingData
986        );
987        // `a=` is a token68 with padding; `a=, b` is a parameter with no value.
988        assert_eq!(
989            parse_credentials(b"Digest a=", &l).unwrap().payload(),
990            AuthPayload::Token68(b"a=")
991        );
992        assert_eq!(
993            parse_credentials(b"Digest a=, b=1", &l).unwrap_err().kind(),
994            ErrorKind::MissingValue
995        );
996        assert_eq!(
997            parse_credentials(b"Digest a=\"x", &l).unwrap_err().kind(),
998            ErrorKind::UnterminatedQuote
999        );
1000    }
1001
1002    #[test]
1003    fn test_credentials_debug_redacted() {
1004        let c = parse_credentials(b"Bearer s3cr3t-token", &Limits::DEFAULT).unwrap();
1005        let d = format!("{c:?} {:?}", c.payload());
1006        assert!(!d.contains("s3cr3t"));
1007        let p = parse_credentials(b"Digest response=\"s3cr3t\"", &Limits::DEFAULT).unwrap();
1008        let d = format!("{:?}", p.payload());
1009        assert_eq!(d, "AuthParams([\"response\"])");
1010    }
1011
1012    #[test]
1013    fn test_challenges_rfc_examples() {
1014        let l = Limits::DEFAULT;
1015        let names = |v: &[u8]| -> Vec<Vec<u8>> {
1016            parse_challenges(v, &l)
1017                .unwrap()
1018                .iter()
1019                .map(|c| c.scheme().to_vec())
1020                .collect()
1021        };
1022        assert_eq!(
1023            names(b"Basic realm=\"a\", Bearer"),
1024            [b"Basic".to_vec(), b"Bearer".to_vec()]
1025        );
1026        let c = parse_challenges(b"Bearer realm=\"a\", error=\"invalid_token\"", &l).unwrap();
1027        let one: Vec<_> = c.iter().collect();
1028        assert_eq!(one.len(), 1);
1029        assert!(one[0].param("error").unwrap().eq_bytes(b"invalid_token"));
1030        assert_eq!(
1031            names(b"Negotiate abc==, Basic realm=x"),
1032            [b"Negotiate".to_vec(), b"Basic".to_vec()]
1033        );
1034        assert_eq!(
1035            names(b"A, , B, ,C"),
1036            [b"A".to_vec(), b"B".to_vec(), b"C".to_vec()]
1037        );
1038        let t68 = parse_challenges(b"Negotiate abc==", &l).unwrap();
1039        assert_eq!(
1040            t68.iter().next().unwrap().payload(),
1041            AuthPayload::Token68(b"abc==")
1042        );
1043    }
1044
1045    #[test]
1046    fn test_challenge_refusals() {
1047        let l = Limits::DEFAULT;
1048        let k = |v: &[u8]| parse_challenges(v, &l).unwrap_err().kind();
1049        assert_eq!(k(b"Basic realm=a, realm=b"), ErrorKind::DuplicateParameter);
1050        assert_eq!(k(b"realm=a"), ErrorKind::ExpectedSeparator);
1051        assert_eq!(k(b"Basic\trealm=a"), ErrorKind::NotToken);
1052        assert_eq!(k(b"Basic abc def"), ErrorKind::TrailingData);
1053        assert!(parse_challenges(b"Basic realm=a, Digest realm=b", &l).is_ok());
1054    }
1055
1056    #[test]
1057    fn test_challenge_writers_round_trip() {
1058        let mut buf = [0u8; 128];
1059        let mut out = crate::SliceSink::new(&mut buf);
1060        write_challenge(
1061            "Bearer",
1062            &[("realm", "api"), ("scope", "read write")],
1063            &mut out,
1064        )
1065        .unwrap();
1066        let c = parse_challenges(out.written(), &Limits::DEFAULT).unwrap();
1067        let first = c.iter().next().unwrap();
1068        assert!(first.param("realm").unwrap().eq_bytes(b"api"));
1069        assert!(first.param("scope").unwrap().eq_bytes(b"read write"));
1070        assert_eq!(
1071            write_challenge("Bad scheme", &[], &mut out),
1072            Err(WriteError::NotToken)
1073        );
1074        assert_eq!(
1075            write_challenge("Basic", &[("realm", "a"), ("Realm", "b")], &mut out),
1076            Err(WriteError::DuplicateName)
1077        );
1078        assert_eq!(
1079            write_challenge("Basic", &[("realm", "a\r\n")], &mut out),
1080            Err(WriteError::Forbidden { at: 1 })
1081        );
1082        assert_eq!(
1083            write_challenge_token68("Negotiate", b"a b", &mut out),
1084            Err(WriteError::Forbidden { at: 1 })
1085        );
1086    }
1087}