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