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}