1#![cfg_attr(
34 any(feature = "http", feature = "http10"),
35 doc = r##"
36```rust
37use std::convert::TryFrom as _;
38use http_auth::PasswordClient;
39
40let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB";
41let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap();
42assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
43let response = pw_client.respond(&http_auth::PasswordParams {
44 username: "Aladdin",
45 password: "open sesame",
46 uri: "/",
47 method: "GET",
48 body: Some(&[]),
49}).unwrap();
50assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
51```
52"##
53)]
54#![cfg_attr(
59 any(feature = "http", feature = "http10"),
60 doc = r##"
61```rust
62# use std::convert::TryFrom as _;
63use http::header::{HeaderMap, WWW_AUTHENTICATE};
64# use http_auth::PasswordClient;
65
66let mut headers = HeaderMap::new();
67headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap());
68headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap());
69
70let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap();
71assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
72```
73"##
74)]
75#![cfg_attr(docsrs, feature(doc_cfg))]
76
77use std::convert::TryFrom;
78
79pub mod parser;
80
81#[cfg(feature = "basic-scheme")]
82#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
83pub mod basic;
84
85#[cfg(feature = "digest-scheme")]
86#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
87pub mod digest;
88
89mod table;
90
91pub use parser::ChallengeParser;
92
93#[cfg(feature = "basic-scheme")]
94#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
95pub use crate::basic::BasicClient;
96
97#[cfg(feature = "digest-scheme")]
98#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
99pub use crate::digest::DigestClient;
100
101use crate::table::{char_classes, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
102
103#[cfg(feature = "digest-scheme")]
104use crate::table::C_ATTR;
105
106#[derive(Clone, Eq, PartialEq)]
117pub struct ChallengeRef<'i> {
118 pub scheme: &'i str,
120
121 pub params: Vec<ChallengeParamRef<'i>>,
129}
130
131impl<'i> ChallengeRef<'i> {
132 pub fn new(scheme: &'i str) -> Self {
133 ChallengeRef {
134 scheme,
135 params: Vec::new(),
136 }
137 }
138}
139
140impl<'i> std::fmt::Debug for ChallengeRef<'i> {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("ChallengeRef")
143 .field("scheme", &self.scheme)
144 .field("params", &ParamsPrinter(&self.params))
145 .finish()
146 }
147}
148
149type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>);
150
151struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]);
152
153impl<'i> std::fmt::Debug for ParamsPrinter<'i> {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_map().entries(self.0.iter().copied()).finish()
156 }
157}
158
159#[cfg_attr(
188 feature = "digest",
189 doc = r##"
190```rust
191use http_auth::PasswordClient;
192let client = PasswordClient::builder()
193 .challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB")
194 .challenges("Digest \
195 realm=\"http-auth@example.org\", \
196 qop=\"auth, auth-int\", \
197 algorithm=MD5, \
198 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
199 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"")
200 .build()
201 .unwrap();
202assert!(matches!(client, PasswordClient::Digest(_)));
203```
204"##
205)]
206#[derive(Default)]
207pub struct PasswordClientBuilder(
208 Option<Result<PasswordClient, String>>,
213);
214
215pub struct ToStrError {
217 _priv: (),
218}
219
220#[cfg(any(feature = "http", feature = "http10"))]
222pub trait HeaderValue {
223 fn to_str(&self) -> Result<&str, ToStrError>;
224}
225
226#[cfg(feature = "http")]
227impl HeaderValue for http::HeaderValue {
228 fn to_str(&self) -> Result<&str, ToStrError> {
229 self.to_str().map_err(|_| ToStrError { _priv: () })
230 }
231}
232
233#[cfg(feature = "http10")]
234impl HeaderValue for http10::HeaderValue {
235 fn to_str(&self) -> Result<&str, ToStrError> {
236 self.to_str().map_err(|_| ToStrError { _priv: () })
237 }
238}
239
240impl PasswordClientBuilder {
241 #[cfg(any(feature = "http", feature = "http10"))]
243 #[cfg_attr(docsrs, doc(cfg(any(feature = "http", feature = "http10"))))]
244 pub fn header_value<V: HeaderValue>(mut self, value: &V) -> Self {
245 if self.complete() {
246 return self;
247 }
248
249 match value.to_str() {
250 Ok(v) => self = self.challenges(v),
251 Err(_) if matches!(self.0, None) => self.0 = Some(Err("non-ASCII header value".into())),
252 _ => {}
253 }
254
255 self
256 }
257
258 #[cfg(feature = "digest-scheme")]
260 fn complete(&self) -> bool {
261 matches!(self.0, Some(Ok(PasswordClient::Digest(_))))
262 }
263
264 #[cfg(not(feature = "digest-scheme"))]
266 fn complete(&self) -> bool {
267 matches!(self.0, Some(Ok(_)))
268 }
269
270 pub fn challenges(mut self, value: &str) -> Self {
272 let mut parser = ChallengeParser::new(value);
273 while !self.complete() {
274 match parser.next() {
275 Some(Ok(c)) => self = self.challenge(&c),
276 Some(Err(e)) if self.0.is_none() => self.0 = Some(Err(e.to_string())),
277 _ => break,
278 }
279 }
280 self
281 }
282
283 pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self {
285 if self.complete() {
286 return self;
287 }
288
289 #[cfg(feature = "digest-scheme")]
290 if challenge.scheme.eq_ignore_ascii_case("Digest") {
291 match DigestClient::try_from(challenge) {
292 Ok(c) => self.0 = Some(Ok(PasswordClient::Digest(c))),
293 Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
294 _ => {}
295 }
296 return self;
297 }
298
299 #[cfg(feature = "basic-scheme")]
300 if challenge.scheme.eq_ignore_ascii_case("Basic") && !matches!(self.0, Some(Ok(_))) {
301 match BasicClient::try_from(challenge) {
302 Ok(c) => self.0 = Some(Ok(PasswordClient::Basic(c))),
303 Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
304 _ => {}
305 }
306 return self;
307 }
308
309 if self.0.is_none() {
310 self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme)));
311 }
312
313 self
314 }
315
316 pub fn build(self) -> Result<PasswordClient, String> {
318 self.0.unwrap_or_else(|| Err("no challenges given".into()))
319 }
320}
321
322#[derive(Debug, Eq, PartialEq)]
331#[non_exhaustive]
332pub enum PasswordClient {
333 #[cfg(feature = "basic-scheme")]
334 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
335 Basic(BasicClient),
336
337 #[cfg(feature = "digest-scheme")]
338 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
339 Digest(DigestClient),
340}
341
342impl TryFrom<&ChallengeRef<'_>> for PasswordClient {
346 type Error = String;
347
348 fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
349 #[cfg(feature = "basic-scheme")]
350 if value.scheme.eq_ignore_ascii_case("Basic") {
351 return Ok(PasswordClient::Basic(BasicClient::try_from(value)?));
352 }
353 #[cfg(feature = "digest-scheme")]
354 if value.scheme.eq_ignore_ascii_case("Digest") {
355 return Ok(PasswordClient::Digest(DigestClient::try_from(value)?));
356 }
357
358 Err(format!("unsupported challenge scheme {:?}", value.scheme))
359 }
360}
361
362impl TryFrom<&str> for PasswordClient {
366 type Error = String;
367
368 #[inline]
369 fn try_from(value: &str) -> Result<Self, Self::Error> {
370 PasswordClient::builder().challenges(value).build()
371 }
372}
373
374#[cfg(feature = "http")]
378#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
379impl TryFrom<&http::HeaderValue> for PasswordClient {
380 type Error = String;
381
382 #[inline]
383 fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
384 PasswordClient::builder().header_value(value).build()
385 }
386}
387
388#[cfg(feature = "http10")]
392#[cfg_attr(docsrs, doc(cfg(feature = "http10")))]
393impl TryFrom<&http10::HeaderValue> for PasswordClient {
394 type Error = String;
395
396 #[inline]
397 fn try_from(value: &http10::HeaderValue) -> Result<Self, Self::Error> {
398 PasswordClient::builder().header_value(value).build()
399 }
400}
401
402#[cfg(feature = "http")]
406#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
407impl TryFrom<http::header::GetAll<'_, http::HeaderValue>> for PasswordClient {
408 type Error = String;
409
410 fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> {
411 let mut builder = PasswordClient::builder();
412 for v in value {
413 builder = builder.header_value(v);
414 }
415 builder.build()
416 }
417}
418
419#[cfg(feature = "http10")]
423#[cfg_attr(docsrs, doc(cfg(feature = "http10")))]
424impl TryFrom<http10::header::GetAll<'_, http10::HeaderValue>> for PasswordClient {
425 type Error = String;
426
427 fn try_from(
428 value: http10::header::GetAll<'_, http10::HeaderValue>,
429 ) -> Result<Self, Self::Error> {
430 let mut builder = PasswordClient::builder();
431 for v in value {
432 builder = builder.header_value(v);
433 }
434 builder.build()
435 }
436}
437
438impl PasswordClient {
439 pub fn builder() -> PasswordClientBuilder {
443 PasswordClientBuilder::default()
444 }
445
446 #[allow(unused_variables)] pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> {
452 match self {
453 #[cfg(feature = "basic-scheme")]
454 Self::Basic(c) => Ok(c.respond(p.username, p.password)),
455 #[cfg(feature = "digest-scheme")]
456 Self::Digest(c) => c.respond(p),
457
458 #[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))]
462 _ => unreachable!(),
463 }
464 }
465}
466
467#[derive(Copy, Clone, Debug, Eq, PartialEq)]
484pub struct PasswordParams<'a> {
485 pub username: &'a str,
486 pub password: &'a str,
487
488 pub uri: &'a str,
507
508 pub method: &'a str,
513
514 pub body: Option<&'a [u8]>,
521}
522
523#[inline]
555pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> {
556 parser::ChallengeParser::new(input).collect()
557}
558
559#[derive(Copy, Clone, Eq, PartialEq)]
561pub struct ParamValue<'i> {
562 escapes: usize,
564
565 escaped: &'i str,
568}
569
570impl<'i> ParamValue<'i> {
571 pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> {
575 let mut escapes = 0;
576 let mut pos = 0;
577 while pos < escaped.len() {
578 let slash = memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).map(|off| pos + off);
579 for i in pos..slash.unwrap_or(escaped.len()) {
580 if (char_classes(escaped.as_bytes()[i]) & C_QDTEXT) == 0 {
581 return Err(format!("{:?} has non-qdtext at byte {}", escaped, i));
582 }
583 }
584 if let Some(slash) = slash {
585 escapes += 1;
586 if escaped.len() <= slash + 1 {
587 return Err(format!("{:?} ends at a quoted-pair escape", escaped));
588 }
589 if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 {
590 return Err(format!(
591 "{:?} has an invalid quote-pair escape at byte {}",
592 escaped,
593 slash + 1
594 ));
595 }
596 pos = slash + 2;
597 } else {
598 break;
599 }
600 }
601 Ok(Self { escaped, escapes })
602 }
603
604 #[doc(hidden)]
607 pub fn new(escapes: usize, escaped: &'i str) -> Self {
608 let mut pos = 0;
609 for escape in 0..escapes {
610 match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) {
611 Some(rel_pos) => pos += rel_pos + 2,
612 None => panic!(
613 "expected {} backslashes in {:?}, ran out after {}",
614 escapes, escaped, escape
615 ),
616 };
617 }
618 if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() {
619 panic!(
620 "expected {} backslashes in {:?}, are more",
621 escapes, escaped
622 );
623 }
624 ParamValue { escapes, escaped }
625 }
626
627 pub fn append_unescaped(&self, to: &mut String) {
629 to.reserve(self.escaped.len() - self.escapes);
630 let mut first_unwritten = 0;
631 for _ in 0..self.escapes {
632 let i = match memchr::memchr(b'\\', &self.escaped.as_bytes()[first_unwritten..]) {
633 Some(rel_i) => first_unwritten + rel_i,
634 None => panic!("bad ParamValues; not as many backslash escapes as promised"),
635 };
636 to.push_str(&self.escaped[first_unwritten..i]);
637 to.push_str(&self.escaped[i + 1..i + 2]);
638 first_unwritten = i + 2;
639 }
640 to.push_str(&self.escaped[first_unwritten..]);
641 }
642
643 #[inline]
645 pub fn unescaped_len(&self) -> usize {
646 self.escaped.len() - self.escapes
647 }
648
649 pub fn to_unescaped(&self) -> String {
651 let mut to = String::new();
652 self.append_unescaped(&mut to);
653 to
654 }
655
656 #[cfg(feature = "digest-scheme")]
658 fn unescaped_with_scratch<'tmp>(&self, scratch: &'tmp mut String) -> &'tmp str
659 where
660 'i: 'tmp,
661 {
662 if self.escapes == 0 {
663 self.escaped
664 } else {
665 let start = scratch.len();
666 self.append_unescaped(scratch);
667 &scratch[start..]
668 }
669 }
670
671 #[inline]
673 pub fn as_escaped(&self) -> &'i str {
674 self.escaped
675 }
676}
677
678impl<'i> std::fmt::Debug for ParamValue<'i> {
679 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680 write!(f, "\"{}\"", self.escaped)
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use crate::ParamValue;
687 use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
688
689 #[test]
695 fn table() {
696 println!("oct dec hex char tchar qdtext escapable ows attr");
698 for b in 0..128 {
699 let classes = crate::char_classes(b);
700 let if_class =
701 |class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" };
702 println!(
703 "{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}",
704 b,
705 b,
706 b,
707 format!("{:?}", char::from(b)),
708 if_class(C_TCHAR, "tchar"),
709 if_class(C_QDTEXT, "qdtext"),
710 if_class(C_ESCAPABLE, "escapable"),
711 if_class(C_OWS, "ows"),
712 if_class(C_ATTR, "attr")
713 );
714
715 assert!(classes & (C_TCHAR | C_QDTEXT) != C_TCHAR);
718 assert!(classes & (C_OWS | C_QDTEXT) != C_OWS);
719 assert!(classes & (C_QDTEXT | C_ESCAPABLE) != C_QDTEXT);
720 }
721 }
722
723 #[test]
724 fn try_from_escaped() {
725 assert_eq!(ParamValue::try_from_escaped("").unwrap().escapes, 0);
726 assert_eq!(ParamValue::try_from_escaped("foo").unwrap().escapes, 0);
727 assert_eq!(ParamValue::try_from_escaped("\\\"").unwrap().escapes, 1);
728 assert_eq!(
729 ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes,
730 1
731 );
732 assert_eq!(
733 ParamValue::try_from_escaped("foo\\\"bar\\\"baz")
734 .unwrap()
735 .escapes,
736 2
737 );
738 ParamValue::try_from_escaped("\\").unwrap_err(); ParamValue::try_from_escaped("\"").unwrap_err(); ParamValue::try_from_escaped("\n").unwrap_err(); ParamValue::try_from_escaped("\\\n").unwrap_err(); }
743
744 #[test]
745 fn unescape() {
746 assert_eq!(
747 &ParamValue {
748 escapes: 0,
749 escaped: ""
750 }
751 .to_unescaped(),
752 ""
753 );
754 assert_eq!(
755 &ParamValue {
756 escapes: 0,
757 escaped: "foo"
758 }
759 .to_unescaped(),
760 "foo"
761 );
762 assert_eq!(
763 &ParamValue {
764 escapes: 1,
765 escaped: "\\foo"
766 }
767 .to_unescaped(),
768 "foo"
769 );
770 assert_eq!(
771 &ParamValue {
772 escapes: 1,
773 escaped: "fo\\o"
774 }
775 .to_unescaped(),
776 "foo"
777 );
778 assert_eq!(
779 &ParamValue {
780 escapes: 1,
781 escaped: "foo\\bar"
782 }
783 .to_unescaped(),
784 "foobar"
785 );
786 assert_eq!(
787 &ParamValue {
788 escapes: 3,
789 escaped: "\\foo\\ba\\r"
790 }
791 .to_unescaped(),
792 "foobar"
793 );
794 }
795}