Skip to content

Commit

Permalink
refactor(client): use select_ok instead of join_all for faster responses
Browse files Browse the repository at this point in the history
  • Loading branch information
Guusvanmeerveld committed Sep 24, 2023
1 parent a3a39cc commit 623b78e
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 99 deletions.
17 changes: 9 additions & 8 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ impl Client {
Ok(client)
}

async fn handle_config_result(
async fn handle_config_result<'a>(
&self,
result: AutodiscoverResult,
mut request: AutodiscoverRequest,
mut request: AutodiscoverRequest<'a>,
) -> Result<AutodiscoverResponse> {
match result {
AutodiscoverResult::Ok(config) => Ok(config),
Expand Down Expand Up @@ -68,7 +68,7 @@ impl Client {

#[async_recursion]
/// Send an autodiscover request to an Exchange server.
pub async fn send_request<R: Into<AutodiscoverRequest> + Send>(
pub async fn send_request<'a, R: Into<AutodiscoverRequest<'a>> + Send>(
&self,
request: R,
) -> Result<AutodiscoverResponse> {
Expand Down Expand Up @@ -100,11 +100,12 @@ impl Client {
return Ok(config);
}

pub async fn dns_query<D: AsRef<str>>(&self, domain: D) -> Result<(String, u16)> {
let (fqdn, port) = self.dns.srv_lookup(domain).await?;
pub async fn dns_query<D: AsRef<str>>(&self, domain: D) -> Result<Vec<(String, u16)>> {
let results = self.dns.srv_lookup(domain).await?;

let domain_name = fqdn.trim_end_matches('.').to_string();

Ok((domain_name, port))
Ok(results
.into_iter()
.map(|(fqdn, port)| (fqdn.trim_end_matches('.').to_string(), port))
.collect())
}
}
16 changes: 6 additions & 10 deletions src/dns.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::cmp::Ordering;

use crate::error::{err, ErrorKind, Result};
use crate::error::Result;

use trust_dns_resolver::{config::ResolverConfig, proto::rr::rdata::SRV};

Expand Down Expand Up @@ -82,21 +82,17 @@ impl Dns {
/// Lookup an SRV record for a given domain.
///
/// Returns the record with the highest priority and weight.
pub async fn srv_lookup<D: AsRef<str>>(&self, domain: D) -> Result<(String, u16)> {
pub async fn srv_lookup<D: AsRef<str>>(&self, domain: D) -> Result<Vec<(String, u16)>> {
let records = self.resolver.srv_lookup(domain.as_ref()).await?;

let mut weighted_records: Vec<_> =
records.into_iter().map(WeightedSrvRecord::new).collect();

weighted_records.sort();

if let Some(record) = weighted_records.first() {
return Ok((record.record.target().to_string(), record.record.port()));
}

err!(
ErrorKind::NotFound,
"Could not find any domains from the SRV query"
)
Ok(weighted_records
.into_iter()
.map(|srv| (srv.record.target().to_string(), srv.record.port()))
.collect())
}
}
120 changes: 53 additions & 67 deletions src/email.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use futures::future::join_all;
use futures::future::select_ok;

use log::warn;
use validator::validate_email;
Expand All @@ -14,21 +14,23 @@ const INVALID_EMAIL_MESSAGE: &str = "The given email address is invalid";
/// Parse the domain name from an email address.
///
/// Also validates that the given string is an email address.
fn domain_from_email<E: AsRef<str>>(email: E) -> Result<String> {
if !validate_email(email.as_ref()) {
fn domain_from_email<'a>(email: &'a str) -> Result<&'a str> {
if !validate_email(email) {
err!(ErrorKind::InvalidEmailAddress, "{}", INVALID_EMAIL_MESSAGE);
};

let mut email_split = email.as_ref().split('@');
let domain = {
let mut email_split = email.split('@');

email_split.next();
email_split.next();

let domain = match email_split.next() {
Some(domain) => domain,
None => err!(ErrorKind::InvalidEmailAddress, "{}", INVALID_EMAIL_MESSAGE),
match email_split.next() {
Some(domain) => domain,
None => err!(ErrorKind::InvalidEmailAddress, "{}", INVALID_EMAIL_MESSAGE),
}
};

Ok(domain.to_string())
Ok(domain)
}

/// Fetch an autodiscover config from a given email address and password.
Expand All @@ -39,6 +41,16 @@ pub async fn from_email<E: AsRef<str>, P: AsRef<str>, U: AsRef<str>>(
password: Option<P>,
username: Option<U>,
) -> Result<AutodiscoverResponse> {
let creds = (
username
.as_ref()
.map(|username| username.as_ref())
.unwrap_or(email.as_ref()),
password.as_ref().map(|pass| pass.as_ref()),
);

let client = Client::new(creds).await?;

let domain = domain_from_email(email.as_ref())?;

// In this function we follow the steps to autodiscovery as specified by the following:
Expand Down Expand Up @@ -72,41 +84,17 @@ pub async fn from_email<E: AsRef<str>, P: AsRef<str>, U: AsRef<str>>(
),
];

let creds = (
username
.as_ref()
.map(|username| username.as_ref())
.unwrap_or(email.as_ref()),
password.as_ref().map(|pass| pass.as_ref()),
);

let client = Client::new(creds).await?;
let mut futures = Vec::new();

let mut requests = Vec::new();
let mut errors: Vec<Error> = Vec::new();

for candidate in candidates {
let request = AutodiscoverRequest::new(candidate, email.as_ref(), true);

// We then send an authenticated request to each candidate.
let future = client.send_request(request);

requests.push(future);
}

let results = join_all(requests).await;

let mut errors: Vec<Error> = Vec::new();

// If any of the urls are a hit, we return it.
for result in results {
match result {
Ok(config) => return Ok(config),
Err(error) => {
warn!("{:?}", error);

errors.push(error);
}
}
futures.push(future);
}

#[cfg(feature = "pox")]
Expand All @@ -120,34 +108,32 @@ pub async fn from_email<E: AsRef<str>, P: AsRef<str>, U: AsRef<str>>(

let request = AutodiscoverRequest::new(candidate, email.as_ref(), false);

let response = client.send_request(request).await;
let future = client.send_request(request);

match response {
Ok(config) => return Ok(config),
Err(error) => {
warn!("{:?}", error);
errors.push(error);
}
};
futures.push(future);
}

// Finally, if all else failed, we try a dns query to try and resolve a domain from there.
match client
.dns_query(format!("_autodiscover._tcp.{}", domain))
.await
{
Ok((autodiscover_domain, autodiscover_port)) => {
let candidates: Vec<String> = vec![
#[cfg(feature = "pox")]
format!(
"https://{}:{}/autodiscover/autodiscover.{}",
autodiscover_domain,
autodiscover_port,
Protocol::POX
),
];
Ok(records) => {
let mut candidates: Vec<String> = Vec::new();

let mut futures: Vec<_> = Vec::new();
for (domain, port) in records {
#[cfg(feature = "pox")]
{
let pox_candidate = format!(
"https://{}:{}/autodiscover/autodiscover.{}",
domain,
port,
Protocol::POX
);

candidates.push(pox_candidate)
}
}

for candidate in candidates {
let request = AutodiscoverRequest::new(candidate, email.as_ref(), true);
Expand All @@ -157,25 +143,25 @@ pub async fn from_email<E: AsRef<str>, P: AsRef<str>, U: AsRef<str>>(

futures.push(future);
}

let responses = join_all(futures).await;

for response in responses {
match response {
Ok(config) => return Ok(config),
Err(error) => {
warn!("{:?}", error);
errors.push(error)
}
};
}
}
Err(error) => {
warn!("{:?}", error);
errors.push(error)
}
};

if futures.is_empty() {
err!(ErrorKind::ConfigNotFound(errors), "No urls to request")
}

let result = select_ok(futures).await;

// If any of the urls are a hit, we return it.
match result {
Ok((config, _remaining)) => return Ok(config),
Err(error) => errors.push(error),
}

// If nothing return a valid configuration, we return an error.
err!(
ErrorKind::ConfigNotFound(errors),
Expand Down
6 changes: 4 additions & 2 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Http {
const TIMEOUT: Duration = Duration::from_secs(10);

pub fn new() -> Result<Self> {
let client = match Config::new().set_timeout(Some(Self::TIMEOUT)).try_into() {
let client: surf::Client = match Config::new().set_timeout(Some(Self::TIMEOUT)).try_into() {
Ok(client) => client,
Err(err) => err!(
ErrorKind::BuildHttpClient,
Expand All @@ -65,7 +65,9 @@ impl Http {
),
};

let http = Self { client };
let http = Self {
client: client.with(surf::middleware::Redirect::new(5)),
};

Ok(http)
}
Expand Down
8 changes: 5 additions & 3 deletions src/types/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ impl Protocol {
Protocol::POX => {
let request_config = PoxAutodiscover::create_request(email_address.as_ref());

let config_string = serde_xml_rs::to_string(&request_config)?;
let mut buf = Vec::new();

debug!("Request configuration: {}", config_string);
serde_xml_rs::to_writer(&mut buf, &request_config)?;

Ok(config_string.into())
debug!("Request configuration: {}", String::from_utf8_lossy(&buf));

Ok(buf.into())
}
_ => Ok(Bytes::new()),
}
Expand Down
24 changes: 15 additions & 9 deletions src/types/request.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
use std::borrow::Cow;

use bytes::Bytes;

use crate::error::{err, ErrorKind, Result};

use super::protocol::Protocol;

pub struct AutodiscoverRequest {
pub struct AutodiscoverRequest<'a> {
use_auth: bool,
url: String,
email: String,
url: Cow<'a, str>,
email: Cow<'a, str>,
}

impl AutodiscoverRequest {
pub fn new<U: Into<String>, E: Into<String>>(url: U, email: E, use_auth: bool) -> Self {
impl<'a> AutodiscoverRequest<'a> {
pub fn new<U: Into<Cow<'a, str>>, E: Into<Cow<'a, str>>>(
url: U,
email: E,
use_auth: bool,
) -> Self {
Self {
url: url.into(),
email: email.into(),
use_auth,
}
}

pub fn set_url(&mut self, url: String) {
self.url = url;
pub fn set_url<U: Into<Cow<'a, str>>>(&mut self, url: U) {
self.url = url.into();
}

pub fn set_email(&mut self, email: String) {
self.email = email;
pub fn set_email<E: Into<Cow<'a, str>>>(&mut self, email: E) {
self.email = email.into();
}

pub fn protocol(&self) -> Result<Protocol> {
Expand Down

0 comments on commit 623b78e

Please sign in to comment.