#![cfg_attr(
feature = "http",
doc = r##"
```rust
use std::convert::TryFrom as _;
use http_auth::PasswordClient;
let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB";
let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap();
assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
let response = pw_client.respond(&http_auth::PasswordParams {
username: "Aladdin",
password: "open sesame",
uri: "/",
method: "GET",
body: Some(&[]),
}).unwrap();
assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
```
"##
)]
#![cfg_attr(
feature = "http",
doc = r##"
```rust
# use std::convert::TryFrom as _;
use http::header::{HeaderMap, WWW_AUTHENTICATE};
# use http_auth::PasswordClient;
let mut headers = HeaderMap::new();
headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap());
headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap());
let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap();
assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
```
"##
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::convert::TryFrom;
pub mod parser;
#[cfg(feature = "basic-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
pub mod basic;
#[cfg(feature = "digest-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
pub mod digest;
pub use parser::ChallengeParser;
#[cfg(feature = "basic-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
pub use crate::basic::BasicClient;
#[cfg(feature = "digest-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
pub use crate::digest::DigestClient;
const C_TCHAR: u8 = 1;
const C_QDTEXT: u8 = 2;
const C_ESCAPABLE: u8 = 4;
const C_OWS: u8 = 8;
#[cfg_attr(not(feature = "digest-scheme"), allow(unused))]
const C_ATTR: u8 = 16;
fn char_classes(b: u8) -> u8 {
const TABLE: &[u8; 128] = include_bytes!(concat!(env!("OUT_DIR"), "/char_class_table.bin"));
if b > 128 {
0
} else {
TABLE[usize::from(b)]
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct ChallengeRef<'i> {
pub scheme: &'i str,
pub params: Vec<ChallengeParamRef<'i>>,
}
impl<'i> ChallengeRef<'i> {
pub fn new(scheme: &'i str) -> Self {
ChallengeRef {
scheme,
params: Vec::new(),
}
}
}
impl<'i> std::fmt::Debug for ChallengeRef<'i> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChallengeRef")
.field("scheme", &self.scheme)
.field("params", &ParamsPrinter(&self.params))
.finish()
}
}
type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>);
struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]);
impl<'i> std::fmt::Debug for ParamsPrinter<'i> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entries(self.0.iter().map(|&(ref k, ref v)| (k, v)))
.finish()
}
}
#[cfg_attr(
feature = "digest",
doc = r##"
```rust
use http_auth::PasswordClient;
let client = PasswordClient::builder()
.challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB")
.challenges("Digest \
realm=\"http-auth@example.org\", \
qop=\"auth, auth-int\", \
algorithm=MD5, \
nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"")
.build()
.unwrap();
assert!(matches!(client, PasswordClient::Digest(_)));
```
"##
)]
#[derive(Default)]
pub struct PasswordClientBuilder(
))` if there is a suitable client.
Option<Result<PasswordClient, String>>,
);
impl PasswordClientBuilder {
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
pub fn header_value(mut self, value: &http::HeaderValue) -> Self {
if self.complete() {
return self;
}
match value.to_str() {
Ok(v) => self = self.challenges(v),
Err(_) if matches!(self.0, None) => self.0 = Some(Err("non-ASCII header value".into())),
_ => {}
}
self
}
#[cfg(feature = "digest-scheme")]
fn complete(&self) -> bool {
matches!(self.0, Some(Ok(PasswordClient::Digest(_))))
}
#[cfg(not(feature = "digest-scheme"))]
fn complete(&self) -> bool {
matches!(self.0, Some(Ok(_)))
}
pub fn challenges(mut self, value: &str) -> Self {
let mut parser = ChallengeParser::new(value);
while !self.complete() {
match parser.next() {
Some(Ok(c)) => self = self.challenge(&c),
Some(Err(e)) if self.0.is_none() => self.0 = Some(Err(e.to_string())),
_ => break,
}
}
self
}
pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self {
if self.complete() {
return self;
}
#[cfg(feature = "digest-scheme")]
if challenge.scheme.eq_ignore_ascii_case("Digest") {
match DigestClient::try_from(challenge) {
Ok(c) => self.0 = Some(Ok(PasswordClient::Digest(c))),
Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
_ => {}
}
return self;
}
#[cfg(feature = "basic-scheme")]
if challenge.scheme.eq_ignore_ascii_case("Basic") && !matches!(self.0, Some(Ok(_))) {
match BasicClient::try_from(challenge) {
Ok(c) => self.0 = Some(Ok(PasswordClient::Basic(c))),
Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
_ => {}
}
return self;
}
if self.0.is_none() {
self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme)));
}
self
}
pub fn build(self) -> Result<PasswordClient, String> {
self.0.unwrap_or_else(|| Err("no challenges given".into()))
}
}
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PasswordClient {
#[cfg(feature = "basic-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
Basic(BasicClient),
#[cfg(feature = "digest-scheme")]
#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
Digest(DigestClient),
}
impl TryFrom<&ChallengeRef<'_>> for PasswordClient {
type Error = String;
fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
#[cfg(feature = "basic-scheme")]
if value.scheme.eq_ignore_ascii_case("Basic") {
return Ok(PasswordClient::Basic(BasicClient::try_from(value)?));
}
#[cfg(feature = "digest-scheme")]
if value.scheme.eq_ignore_ascii_case("Digest") {
return Ok(PasswordClient::Digest(DigestClient::try_from(value)?));
}
Err(format!("unsupported challenge scheme {:?}", value.scheme))
}
}
impl TryFrom<&str> for PasswordClient {
type Error = String;
#[inline]
fn try_from(value: &str) -> Result<Self, Self::Error> {
PasswordClient::builder().challenges(value).build()
}
}
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
impl TryFrom<&http::HeaderValue> for PasswordClient {
type Error = String;
#[inline]
fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
PasswordClient::builder().header_value(value).build()
}
}
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
impl TryFrom<http::header::GetAll<'_, http::HeaderValue>> for PasswordClient {
type Error = String;
fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> {
let mut builder = PasswordClient::builder();
for v in value {
builder = builder.header_value(v);
}
builder.build()
}
}
impl PasswordClient {
pub fn builder() -> PasswordClientBuilder {
PasswordClientBuilder::default()
}
#[allow(unused_variables)] pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> {
match self {
#[cfg(feature = "basic-scheme")]
Self::Basic(c) => Ok(c.respond(p.username, p.password)),
#[cfg(feature = "digest-scheme")]
Self::Digest(c) => c.respond(p),
#[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))]
_ => unreachable!(),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct PasswordParams<'a> {
pub username: &'a str,
pub password: &'a str,
pub uri: &'a str,
pub method: &'a str,
pub body: Option<&'a [u8]>,
}
#[inline]
pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> {
parser::ChallengeParser::new(input).collect()
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct ParamValue<'i> {
escapes: usize,
escaped: &'i str,
}
impl<'i> ParamValue<'i> {
pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> {
let mut escapes = 0;
let mut pos = 0;
while pos < escaped.len() {
let slash = memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).map(|off| pos + off);
for i in pos..slash.unwrap_or(escaped.len()) {
if (char_classes(escaped.as_bytes()[i]) & C_QDTEXT) == 0 {
return Err(format!("{:?} has non-qdtext at byte {}", escaped, i));
}
}
if let Some(slash) = slash {
escapes += 1;
if escaped.len() <= slash + 1 {
return Err(format!("{:?} ends at a quoted-pair escape", escaped));
}
if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 {
return Err(format!(
"{:?} has an invalid quote-pair escape at byte {}",
escaped,
slash + 1
));
}
pos = slash + 2;
} else {
break;
}
}
Ok(Self { escaped, escapes })
}
#[doc(hidden)]
pub fn new(escapes: usize, escaped: &'i str) -> Self {
let mut pos = 0;
for escape in 0..escapes {
match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) {
Some(rel_pos) => pos += rel_pos + 2,
None => panic!(
"expected {} backslashes in {:?}, ran out after {}",
escapes, escaped, escape
),
};
}
if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() {
panic!(
"expected {} backslashes in {:?}, are more",
escapes, escaped
);
}
ParamValue { escapes, escaped }
}
pub fn append_unescaped(&self, to: &mut String) {
to.reserve(self.escaped.len() - self.escapes);
let mut first_unwritten = 0;
for _ in 0..self.escapes {
let i = match memchr::memchr(b'\\', &self.escaped.as_bytes()[first_unwritten..]) {
Some(rel_i) => first_unwritten + rel_i,
None => panic!("bad ParamValues; not as many backslash escapes as promised"),
};
to.push_str(&self.escaped[first_unwritten..i]);
to.push_str(&self.escaped[i + 1..i + 2]);
first_unwritten = i + 2;
}
to.push_str(&self.escaped[first_unwritten..]);
}
#[inline]
pub fn unescaped_len(&self) -> usize {
self.escaped.len() - self.escapes
}
pub fn to_unescaped(&self) -> String {
let mut to = String::new();
self.append_unescaped(&mut to);
to
}
#[cfg(feature = "digest-scheme")]
fn unescaped_with_scratch<'tmp>(&self, scratch: &'tmp mut String) -> &'tmp str
where
'i: 'tmp,
{
if self.escapes == 0 {
self.escaped
} else {
let start = scratch.len();
self.append_unescaped(scratch);
&scratch[start..]
}
}
#[inline]
pub fn as_escaped(&self) -> &'i str {
self.escaped
}
}
impl<'i> std::fmt::Debug for ParamValue<'i> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.escaped)
}
}
#[cfg(test)]
mod tests {
use crate::ParamValue;
use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
#[test]
fn table() {
println!("oct dec hex char tchar qdtext escapable ows attr");
for b in 0..128 {
let classes = crate::char_classes(b);
let if_class =
|class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" };
println!(
"{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}",
b,
b,
b,
format!("{:?}", char::from(b)),
if_class(C_TCHAR, "tchar"),
if_class(C_QDTEXT, "qdtext"),
if_class(C_ESCAPABLE, "escapable"),
if_class(C_OWS, "ows"),
if_class(C_ATTR, "attr")
);
assert!(classes & (C_TCHAR | C_QDTEXT) != C_TCHAR);
assert!(classes & (C_OWS | C_QDTEXT) != C_OWS);
assert!(classes & (C_QDTEXT | C_ESCAPABLE) != C_QDTEXT);
}
}
#[test]
fn try_from_escaped() {
assert_eq!(ParamValue::try_from_escaped("").unwrap().escapes, 0);
assert_eq!(ParamValue::try_from_escaped("foo").unwrap().escapes, 0);
assert_eq!(ParamValue::try_from_escaped("\\\"").unwrap().escapes, 1);
assert_eq!(
ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes,
1
);
assert_eq!(
ParamValue::try_from_escaped("foo\\\"bar\\\"baz")
.unwrap()
.escapes,
2
);
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(); }
#[test]
fn unescape() {
assert_eq!(
&ParamValue {
escapes: 0,
escaped: ""
}
.to_unescaped(),
""
);
assert_eq!(
&ParamValue {
escapes: 0,
escaped: "foo"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
escaped: "\\foo"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
escaped: "fo\\o"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
escaped: "foo\\bar"
}
.to_unescaped(),
"foobar"
);
assert_eq!(
&ParamValue {
escapes: 3,
escaped: "\\foo\\ba\\r"
}
.to_unescaped(),
"foobar"
);
}
}