#![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()
}
}
#[derive(Default)]
pub struct PasswordClientBuilder {
first_err: Option<String>,
cur_client: Option<PasswordClient>,
}
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 self.first_err.is_none() {
self.first_err = Some("non-ASCII header value".into());
}
}
}
self
}
#[cfg(feature = "digest-scheme")]
fn complete(&self) -> bool {
matches!(self.cur_client, Some(PasswordClient::Digest(_)))
}
#[cfg(not(feature = "digest-scheme"))]
fn complete(&self) -> bool {
matches!(self.cur_client, Some(_))
}
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.first_err.is_none() {
self.first_err = Some(e.to_string());
}
}
None => 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.cur_client = Some(PasswordClient::Digest(c)),
Err(e) => {
self.first_err.get_or_insert(e);
}
}
return self;
}
#[cfg(feature = "basic-scheme")]
if challenge.scheme.eq_ignore_ascii_case("Basic") && self.cur_client.is_none() {
match BasicClient::try_from(challenge) {
Ok(c) => self.cur_client = Some(PasswordClient::Basic(c)),
Err(e) => {
self.first_err.get_or_insert(e);
}
}
return self;
}
if self.first_err.is_none() {
self.first_err = Some(format!("Unsupported scheme {:?}", challenge.scheme));
}
self
}
pub fn build(self) -> Result<PasswordClient, String> {
if let Some(c) = self.cur_client {
return Ok(c);
}
if let Some(e) = self.first_err {
return Err(e);
}
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,
raw: &'i str,
}
impl<'i> ParamValue<'i> {
#[doc(hidden)]
pub fn new(escapes: usize, raw: &'i str) -> Self {
let mut pos = 0;
for escape in 0..escapes {
match memchr::memchr(b'\\', &raw.as_bytes()[pos..]) {
Some(rel_pos) => pos += rel_pos + 2,
None => panic!(
"expected {} backslashes in {:?}, ran out after {}",
escapes, raw, escape
),
};
}
if memchr::memchr(b'\\', &raw.as_bytes()[pos..]).is_some() {
panic!("expected {} backslashes in {:?}, are more", escapes, raw);
}
ParamValue { escapes, raw }
}
fn append_unescaped(&self, to: &mut String) {
to.reserve(self.raw.len() - self.escapes);
let mut first_unwritten = 0;
for _ in 0..self.escapes {
let i = match memchr::memchr(b'\\', &self.raw.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.raw[first_unwritten..i]);
to.push_str(&self.raw[i + 1..i + 2]);
first_unwritten = i + 2;
}
to.push_str(&self.raw[first_unwritten..]);
}
#[inline]
pub fn unescaped_len(&self) -> usize {
self.raw.len() - self.escapes
}
pub fn to_unescaped(&self) -> String {
let mut to = String::new();
self.append_unescaped(&mut to);
to
}
}
impl<'i> std::fmt::Debug for ParamValue<'i> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.raw)
}
}
#[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 unescape() {
assert_eq!(
&ParamValue {
escapes: 0,
raw: ""
}
.to_unescaped(),
""
);
assert_eq!(
&ParamValue {
escapes: 0,
raw: "foo"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
raw: "\\foo"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
raw: "fo\\o"
}
.to_unescaped(),
"foo"
);
assert_eq!(
&ParamValue {
escapes: 1,
raw: "foo\\bar"
}
.to_unescaped(),
"foobar"
);
assert_eq!(
&ParamValue {
escapes: 3,
raw: "\\foo\\ba\\r"
}
.to_unescaped(),
"foobar"
);
}
}