EhloOkResp Parser + Type

* ehlo_ok_resp returns `EhloOkResp` now
* Converted some &[u8] parser to &str
* Added unit test
* Changed ehlo_line to accept "AUTH=LOGIN PLAIN" (note the "=")
This commit is contained in:
Damian Poddebniak 2020-08-10 14:25:37 +02:00
parent f14357d5b9
commit d41e8118f3
4 changed files with 176 additions and 82 deletions

View File

@ -10,7 +10,7 @@ use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, DQUOTE, SP};
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n}, bytes::streaming::{tag, tag_no_case, take_while, take_while1, take_while_m_n},
combinator::{opt, recognize}, combinator::{map_res, opt, recognize},
multi::many0, multi::many0,
sequence::{delimited, preceded, tuple}, sequence::{delimited, preceded, tuple},
IResult, IResult,
@ -348,10 +348,10 @@ pub fn Argument(input: &[u8]) -> IResult<&[u8], &[u8]> {
} }
/// Domain = sub-domain *("." sub-domain) /// Domain = sub-domain *("." sub-domain)
pub fn Domain(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Domain(input: &[u8]) -> IResult<&[u8], &str> {
let parser = tuple((sub_domain, many0(tuple((tag(b"."), sub_domain))))); let parser = tuple((sub_domain, many0(tuple((tag(b"."), sub_domain)))));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = map_res(recognize(parser), std::str::from_utf8)(input)?;
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
@ -389,18 +389,21 @@ pub fn Ldh_str(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// General-address-literal /// General-address-literal
/// ) "]" /// ) "]"
/// ; See Section 4.1.3 /// ; See Section 4.1.3
pub fn address_literal(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn address_literal(input: &[u8]) -> IResult<&[u8], &str> {
let parser = delimited( let parser = delimited(
tag(b"["), tag(b"["),
map_res(
alt(( alt((
IPv4_address_literal, IPv4_address_literal,
IPv6_address_literal, IPv6_address_literal,
General_address_literal, General_address_literal,
)), )),
std::str::from_utf8,
),
tag(b"]"), tag(b"]"),
); );
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = parser(input)?;
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
@ -493,3 +496,43 @@ pub fn String(input: &[u8]) -> IResult<&[u8], &[u8]> {
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
#[cfg(test)]
mod test {
use super::{ehlo, helo, mail, sub_domain};
use crate::types::Command;
#[test]
fn test_subdomain() {
let (rem, parsed) = sub_domain(b"example???").unwrap();
assert_eq!(parsed, b"example");
assert_eq!(rem, b"???");
}
#[test]
fn test_ehlo() {
let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap();
assert_eq!(parsed, Command::Ehlo(b"123.123.123.123".to_vec()));
assert_eq!(rem, b"???");
}
#[test]
fn test_helo() {
let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap();
assert_eq!(parsed, Command::Helo(b"example.com".to_vec()));
assert_eq!(rem, b"???");
}
#[test]
fn test_mail() {
let (rem, parsed) = mail(b"MAIL FROM:<userx@y.foo.org>\r\n???").unwrap();
assert_eq!(
parsed,
Command::Mail {
data: b"<userx@y.foo.org>".to_vec(),
params: None
}
);
assert_eq!(rem, b"???");
}
}

View File

@ -1,11 +1,12 @@
use crate::parse::command::Domain; use crate::{parse::command::Domain, types::EhloOkResp};
use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP}; use abnf_core::streaming::{is_ALPHA, is_DIGIT, CRLF, SP};
use nom::multi::separated_list;
use nom::{ use nom::{
branch::alt, branch::alt,
bytes::streaming::{tag, take_while, take_while1, take_while_m_n}, bytes::streaming::{tag, take_while, take_while1, take_while_m_n},
combinator::{opt, recognize}, combinator::{map, map_res, opt, recognize},
multi::many0, multi::many0,
sequence::tuple, sequence::{delimited, preceded, tuple},
IResult, IResult,
}; };
@ -13,29 +14,56 @@ use nom::{
/// ( "250-" Domain [ SP ehlo-greet ] CRLF /// ( "250-" Domain [ SP ehlo-greet ] CRLF
/// *( "250-" ehlo-line CRLF ) /// *( "250-" ehlo-line CRLF )
/// "250" SP ehlo-line CRLF ) /// "250" SP ehlo-line CRLF )
pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], EhloOkResp> {
let parser = alt(( let parser = alt((
recognize(tuple(( map(
tag(b"250"), tuple((tag(b"250"), SP, Domain, opt(preceded(SP, ehlo_greet)), CRLF)),
SP, |(_, _, domain, maybe_ehlo, _)| EhloOkResp {
Domain, domain: domain.to_owned(),
opt(tuple((SP, ehlo_greet))), greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
CRLF, lines: Vec::new(),
))), },
recognize(tuple(( ),
map(
tuple((
tag(b"250-"), tag(b"250-"),
Domain, Domain,
opt(tuple((SP, ehlo_greet))), opt(preceded(SP, ehlo_greet)),
CRLF, CRLF,
many0(tuple((tag(b"250-"), ehlo_line, CRLF))), many0(delimited(tag(b"250-"), ehlo_line, CRLF)),
tag(b"250"), tag(b"250"),
SP, SP,
ehlo_line, ehlo_line,
CRLF, CRLF,
))), )),
|(_, domain, maybe_ehlo, _, lines, _, _, (keyword, params), _)| EhloOkResp {
domain: domain.to_owned(),
greet: maybe_ehlo.map(|ehlo| ehlo.to_owned()),
lines: {
let mut lines = lines
.iter()
.map(|(keyword, params)| {
let params = params
.iter()
.map(|param| param.to_string())
.collect::<Vec<String>>();
(keyword.to_string(), params)
})
.collect::<Vec<(String, Vec<String>)>>();
lines.push((
keyword.to_string(),
params
.iter()
.map(|param| param.to_string())
.collect::<Vec<String>>(),
));
lines
},
},
),
)); ));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, parsed) = parser(input)?;
Ok((remaining, parsed)) Ok((remaining, parsed))
} }
@ -43,7 +71,7 @@ pub fn ehlo_ok_rsp(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// String of any characters other than CR or LF. /// String of any characters other than CR or LF.
/// ///
/// ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127) /// ehlo-greet = 1*(%d0-9 / %d11-12 / %d14-127)
pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &str> {
fn is_valid_character(byte: u8) -> bool { fn is_valid_character(byte: u8) -> bool {
match byte { match byte {
0..=9 | 11..=12 | 14..=127 => true, 0..=9 | 11..=12 | 14..=127 => true,
@ -51,16 +79,24 @@ pub fn ehlo_greet(input: &[u8]) -> IResult<&[u8], &[u8]> {
} }
} }
take_while1(is_valid_character)(input) map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
} }
/// ehlo-line = ehlo-keyword *( SP ehlo-param ) /// ehlo-line = ehlo-keyword *( SP ehlo-param )
pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], &[u8]> { ///
let parser = tuple((ehlo_keyword, many0(tuple((SP, ehlo_param))))); /// TODO: SMTP servers often respond with "AUTH=LOGIN PLAIN". Why?
pub fn ehlo_line(input: &[u8]) -> IResult<&[u8], (&str, Vec<&str>)> {
let parser = tuple((
map_res(ehlo_keyword, std::str::from_utf8),
opt(preceded(
alt((SP, tag("="))), // TODO: For Outlook?
separated_list(SP, ehlo_param),
)),
));
let (remaining, parsed) = recognize(parser)(input)?; let (remaining, (ehlo_keyword, ehlo_params)) = parser(input)?;
Ok((remaining, parsed)) Ok((remaining, (ehlo_keyword, ehlo_params.unwrap_or(vec![]))))
} }
/// Additional syntax of ehlo-params depends on ehlo-keyword /// Additional syntax of ehlo-params depends on ehlo-keyword
@ -81,7 +117,7 @@ pub fn ehlo_keyword(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// (US-ASCII 0-31 and 127 inclusive) /// (US-ASCII 0-31 and 127 inclusive)
/// ///
/// ehlo-param = 1*(%d33-126) /// ehlo-param = 1*(%d33-126)
pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &str> {
fn is_valid_character(byte: u8) -> bool { fn is_valid_character(byte: u8) -> bool {
match byte { match byte {
33..=126 => true, 33..=126 => true,
@ -89,5 +125,52 @@ pub fn ehlo_param(input: &[u8]) -> IResult<&[u8], &[u8]> {
} }
} }
take_while1(is_valid_character)(input) map_res(take_while1(is_valid_character), std::str::from_utf8)(input)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_ehlo_ok_rsp() {
let (rem, out) = ehlo_ok_rsp(
b"250-example.org hello\r\n\
250-AUTH LOGIN CRAM-MD5 PLAIN\r\n\
250-AUTH=LOGIN CRAM-MD5 PLAIN\r\n\
250-STARTTLS\r\n\
250-SIZE 12345\r\n\
250 8BITMIME\r\n",
)
.unwrap();
assert_eq!(rem, b"");
assert_eq!(
out,
EhloOkResp {
domain: "example.org".into(),
greet: Some("hello".into()),
lines: vec![
(
"AUTH".into(),
vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()]
),
(
"AUTH".into(),
vec!["LOGIN".into(), "CRAM-MD5".into(), "PLAIN".into()]
),
("STARTTLS".into(), vec![]),
("SIZE".into(), vec!["12345".into()]),
("8BITMIME".into(), vec![]),
],
}
);
}
#[test]
fn test_ehlo_line() {
let (rem, (keyword, params)) = ehlo_line(b"SIZE 123456\r\n").unwrap();
assert_eq!(rem, b"\r\n");
assert_eq!(keyword, "SIZE");
assert_eq!(params, &["123456"]);
}
} }

View File

@ -79,7 +79,7 @@ pub fn By_domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// ( address-literal FWS "(" TCP-info ")" ) /// ( address-literal FWS "(" TCP-info ")" )
pub fn Extended_Domain(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn Extended_Domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = alt(( let parser = alt((
Domain, recognize(Domain),
recognize(tuple((Domain, FWS, tag(b"("), TCP_info, tag(b")")))), recognize(tuple((Domain, FWS, tag(b"("), TCP_info, tag(b")")))),
recognize(tuple(( recognize(tuple((
address_literal, address_literal,
@ -100,7 +100,7 @@ pub fn Extended_Domain(input: &[u8]) -> IResult<&[u8], &[u8]> {
/// TCP-info = address-literal / ( Domain FWS address-literal ) /// TCP-info = address-literal / ( Domain FWS address-literal )
pub fn TCP_info(input: &[u8]) -> IResult<&[u8], &[u8]> { pub fn TCP_info(input: &[u8]) -> IResult<&[u8], &[u8]> {
let parser = alt(( let parser = alt((
address_literal, recognize(address_literal),
recognize(tuple((Domain, FWS, address_literal))), recognize(tuple((Domain, FWS, address_literal))),
)); ));

View File

@ -90,43 +90,11 @@ impl std::fmt::Debug for Command {
} }
} }
pub type EhloLine = (String, Option<String>); #[derive(Debug, Clone, PartialEq, Eq)]
pub struct EhloOkResp {
#[cfg(test)] pub domain: String,
mod test { pub greet: Option<String>,
use crate::{parse::command::*, types::*}; pub lines: Vec<EhloLine>,
#[test]
fn test_subdomain() {
let (rem, parsed) = sub_domain(b"example???").unwrap();
assert_eq!(parsed, b"example");
assert_eq!(rem, b"???");
} }
#[test] pub type EhloLine = (String, Vec<String>);
fn test_ehlo() {
let (rem, parsed) = ehlo(b"EHLO [123.123.123.123]\r\n???").unwrap();
assert_eq!(parsed, Command::Ehlo(b"[123.123.123.123]".to_vec()));
assert_eq!(rem, b"???");
}
#[test]
fn test_helo() {
let (rem, parsed) = helo(b"HELO example.com\r\n???").unwrap();
assert_eq!(parsed, Command::Helo(b"example.com".to_vec()));
assert_eq!(rem, b"???");
}
#[test]
fn test_mail() {
let (rem, parsed) = mail(b"MAIL FROM:<userx@y.foo.org>\r\n???").unwrap();
assert_eq!(
parsed,
Command::Mail {
data: b"<userx@y.foo.org>".to_vec(),
params: None
}
);
assert_eq!(rem, b"???");
}
}