Files
Stalwart/crates/smtp/src/inbound/auth.rs
2023-05-16 18:25:38 +00:00

238 lines
9.1 KiB
Rust

/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of the Stalwart SMTP Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::{core::Session, lookup::Item};
pub struct SaslToken {
mechanism: u64,
credentials: Credentials<String>,
}
impl SaslToken {
pub fn from_mechanism(mechanism: u64) -> Option<SaslToken> {
match mechanism {
AUTH_PLAIN | AUTH_LOGIN => SaslToken {
mechanism,
credentials: Credentials::Plain {
username: String::new(),
secret: String::new(),
},
}
.into(),
AUTH_OAUTHBEARER => SaslToken {
mechanism,
credentials: Credentials::OAuthBearer {
token: String::new(),
},
}
.into(),
AUTH_XOAUTH2 => SaslToken {
mechanism,
credentials: Credentials::XOauth2 {
username: String::new(),
secret: String::new(),
},
}
.into(),
_ => None,
}
}
}
impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> {
pub async fn handle_sasl_response(
&mut self,
token: &mut SaslToken,
response: &[u8],
) -> Result<bool, ()> {
if response.is_empty() {
match (token.mechanism, &token.credentials) {
(AUTH_PLAIN | AUTH_XOAUTH2 | AUTH_OAUTHBEARER, _) => {
self.write(b"334 Go ahead.\r\n").await?;
return Ok(true);
}
(AUTH_LOGIN, Credentials::Plain { username, secret }) => {
if username.is_empty() && secret.is_empty() {
self.write(b"334 VXNlciBOYW1lAA==\r\n").await?;
return Ok(true);
}
}
_ => (),
}
} else if let Some(response) = base64_decode(response) {
match (token.mechanism, &mut token.credentials) {
(AUTH_PLAIN, Credentials::Plain { username, secret }) => {
let mut b_username = Vec::new();
let mut b_secret = Vec::new();
let mut arg_num = 0;
for ch in response {
if ch != 0 {
if arg_num == 1 {
b_username.push(ch);
} else if arg_num == 2 {
b_secret.push(ch);
}
} else {
arg_num += 1;
}
}
match (String::from_utf8(b_username), String::from_utf8(b_secret)) {
(Ok(s_username), Ok(s_secret)) if !s_username.is_empty() => {
*username = s_username;
*secret = s_secret;
return self
.authenticate(std::mem::take(&mut token.credentials))
.await;
}
_ => (),
}
}
(AUTH_LOGIN, Credentials::Plain { username, secret }) => {
return if username.is_empty() {
*username = response.into_string();
self.write(b"334 UGFzc3dvcmQA\r\n").await?;
Ok(true)
} else {
*secret = response.into_string();
self.authenticate(std::mem::take(&mut token.credentials))
.await
};
}
(AUTH_OAUTHBEARER, Credentials::OAuthBearer { token: token_ }) => {
let response = response.into_string();
if response.contains("auth=") {
*token_ = response;
return self
.authenticate(std::mem::take(&mut token.credentials))
.await;
}
}
(AUTH_XOAUTH2, Credentials::XOauth2 { username, secret }) => {
let mut b_username = Vec::new();
let mut b_secret = Vec::new();
let mut arg_num = 0;
let mut in_arg = false;
for ch in response {
if in_arg {
if ch != 1 {
if arg_num == 1 {
b_username.push(ch);
} else if arg_num == 2 {
b_secret.push(ch);
}
} else {
in_arg = false;
}
} else if ch == b'=' {
arg_num += 1;
in_arg = true;
}
}
match (String::from_utf8(b_username), String::from_utf8(b_secret)) {
(Ok(s_username), Ok(s_secret)) if !s_username.is_empty() => {
*username = s_username;
*secret = s_secret;
return self
.authenticate(std::mem::take(&mut token.credentials))
.await;
}
_ => (),
}
}
_ => (),
}
}
self.auth_error(b"500 5.5.6 Invalid challenge.\r\n").await
}
pub async fn authenticate(&mut self, credentials: Credentials<String>) -> Result<bool, ()> {
if let Some(lookup) = &self.params.auth_lookup {
let authenticated_as = match &credentials {
Credentials::Plain { username, .. }
| Credentials::XOauth2 { username, .. }
| Credentials::OAuthBearer { token: username } => username.to_string(),
};
if let Some(is_authenticated) = lookup
.lookup(Item::Authenticate(credentials))
.await
.map(bool::from)
{
tracing::debug!(
parent: &self.span,
context = "auth",
event = "authenticate",
result = if is_authenticated {"success"} else {"failed"}
);
return if is_authenticated {
self.data.authenticated_as = authenticated_as;
self.eval_post_auth_params().await;
self.write(b"235 2.7.0 Authentication succeeded.\r\n")
.await?;
Ok(false)
} else {
self.auth_error(b"535 5.7.8 Authentication credentials invalid.\r\n")
.await
};
}
} else {
tracing::warn!(
parent: &self.span,
context = "auth",
event = "error",
"No lookup list configured for authentication."
);
}
self.write(b"454 4.7.0 Temporary authentication failure\r\n")
.await?;
Ok(false)
}
pub async fn auth_error(&mut self, response: &[u8]) -> Result<bool, ()> {
tokio::time::sleep(self.params.auth_errors_wait).await;
self.data.auth_errors += 1;
self.write(response).await?;
if self.data.auth_errors < self.params.auth_errors_max {
Ok(false)
} else {
self.write(b"421 4.3.0 Too many authentication errors, disconnecting.\r\n")
.await?;
tracing::debug!(
parent: &self.span,
event = "disconnect",
reason = "auth-errors",
"Too many authentication errors."
);
Err(())
}
}
}