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