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}