Skip to content

Commit a7c6633

Browse files
feat(sasl): added basic sasl support
1 parent db40b42 commit a7c6633

File tree

12 files changed

+303
-44
lines changed

12 files changed

+303
-44
lines changed

Cargo.lock

Lines changed: 31 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,26 @@ rust-version = "1.65.0"
1212
[dependencies]
1313
async-native-tls = { version = "0.5.0", default-features = false }
1414
async-std = { version = "1.12.0", features = ["attributes"], optional = true }
15+
async-trait = { version = "0.1.74", optional = true }
16+
base64 = { version = "0.21.5", optional = true }
1517
bytes = "1.4.0"
1618
futures = "0.3.28"
1719
log = "0.4.20"
1820
nom = "7.1.3"
1921
tokio = { version = "1.26.0", features = [
20-
"net",
21-
"time",
22-
"rt",
23-
"macros",
22+
"net",
23+
"time",
24+
"rt",
25+
"macros",
2426
], optional = true }
2527

2628
[dev-dependencies]
2729
env_logger = "0.10.0"
2830
dotenv = "0.15"
2931

3032
[features]
31-
default = ["runtime-async-std"]
33+
default = ["runtime-async-std", "sasl"]
34+
sasl = ["dep:base64", "dep:async-trait"]
3235

3336
runtime-async-std = ["async-std", "async-native-tls/runtime-async-std"]
3437
runtime-tokio = ["tokio", "async-native-tls/runtime-tokio"]

src/base64.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use base64::engine::{general_purpose::STANDARD, Engine};
2+
use bytes::Bytes;
3+
4+
use crate::error::Result;
5+
6+
pub fn encode<E: AsRef<[u8]>>(encodable: E) -> String {
7+
STANDARD.encode(encodable)
8+
}
9+
10+
pub fn decode<E: AsRef<[u8]>>(decodable: E) -> Result<Bytes> {
11+
Ok(STANDARD.decode(decodable)?.into())
12+
}

src/command.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ pub enum Command {
2222
Quit,
2323
Capa,
2424
Greet,
25-
Other(String),
25+
#[cfg(feature = "sasl")]
26+
Base64(String),
2627
}
2728

2829
impl Display for Command {
2930
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3031
match self {
31-
Self::Other(other) => {
32-
write!(f, "{}", other)?;
32+
#[cfg(feature = "sasl")]
33+
Self::Base64(other) => {
34+
write!(f, "{}", crate::base64::encode(other))?;
3335
}
3436
_ => {
3537
for (key, value) in Self::definitions().into_iter() {

src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum ErrorKind {
2626
ParseInt(ParseIntError),
2727
ParseString(Utf8Error),
2828
ServerError(String),
29+
#[cfg(feature = "sasl")]
30+
DecodeBase64(base64::DecodeError),
2931
NotConnected,
3032
ShouldNotBeConnected,
3133
IncorrectStateForCommand,
@@ -117,6 +119,13 @@ impl From<Utf8Error> for Error {
117119
}
118120
}
119121

122+
#[cfg(feature = "sasl")]
123+
impl From<base64::DecodeError> for Error {
124+
fn from(error: base64::DecodeError) -> Self {
125+
Self::new(ErrorKind::DecodeBase64(error), "Failed to decode base64")
126+
}
127+
}
128+
120129
pub(crate) use err;
121130

122131
pub type Result<T> = result::Result<T, Error>;

src/lib.rs

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ pub mod response;
4949
mod runtime;
5050
mod stream;
5151

52+
#[cfg(feature = "sasl")]
53+
mod base64;
54+
#[cfg(feature = "sasl")]
55+
pub mod sasl;
56+
5257
use std::collections::HashSet;
5358

5459
use async_native_tls::{TlsConnector, TlsStream};
@@ -83,7 +88,7 @@ pub enum ClientState {
8388
None,
8489
}
8590

86-
pub struct Client<S: Write + Read + Unpin> {
91+
pub struct Client<S: Write + Read + Unpin + Send> {
8792
inner: Option<PopStream<S>>,
8893
capabilities: Capabilities,
8994
marked_as_del: Vec<usize>,
@@ -93,7 +98,7 @@ pub struct Client<S: Write + Read + Unpin> {
9398
}
9499

95100
/// Creates a client from a given socket connection.
96-
async fn create_client_from_socket<S: Read + Write + Unpin>(
101+
async fn create_client_from_socket<S: Read + Write + Unpin + Send>(
97102
socket: PopStream<S>,
98103
) -> Result<Client<S>> {
99104
let mut client = Client {
@@ -127,7 +132,7 @@ async fn create_client_from_socket<S: Read + Write + Unpin>(
127132
/// client.quit().unwrap();
128133
/// }
129134
/// ```
130-
pub async fn new<S: Read + Write + Unpin>(stream: S) -> Result<Client<S>> {
135+
pub async fn new<S: Read + Write + Unpin + Send>(stream: S) -> Result<Client<S>> {
131136
let socket = PopStream::new(stream);
132137

133138
create_client_from_socket(socket).await
@@ -159,7 +164,7 @@ pub async fn connect_plain<A: ToSocketAddrs>(addr: A) -> Result<Client<TcpStream
159164
create_client_from_socket(socket).await
160165
}
161166

162-
impl<S: Read + Write + Unpin> Client<S> {
167+
impl<S: Read + Write + Unpin + Send> Client<S> {
163168
/// Check if the client is in the correct state and return a mutable reference to the tcp connection.
164169
fn inner_mut(&mut self) -> Result<&mut PopStream<S>> {
165170
match self.inner.as_mut() {
@@ -529,31 +534,57 @@ impl<S: Read + Write + Unpin> Client<S> {
529534
}
530535
}
531536

532-
// pub async fn auth<A: AsRef<str>, U: AsRef<str>>(
533-
// &mut self,
534-
// auth_type: A,
535-
// token: U,
536-
// ) -> Result<Text> {
537-
// self.check_client_state(ClientState::Authentication)?;
537+
/// ### AUTH
538+
///
539+
/// Requires an [sasl::Authenticator] to work. One could implement this themeselves for any given mechanism, look at the documentation for this trait.
540+
///
541+
/// If a common mechanism is needed, it can probably be found in the [sasl] module.
542+
///
543+
/// The AUTH command indicates an authentication mechanism to the server. If the server supports the requested authentication mechanism, it performs an authentication protocol exchange to authenticate and identify the user. Optionally, it also negotiates a protection mechanism for subsequent protocol interactions. If the requested authentication mechanism is not supported, the server should reject the AUTH command by sending a negative response.
544+
///
545+
/// The authentication protocol exchange consists of a series of server challenges and client answers that are specific to the authentication mechanism. A server challenge, otherwise known as a ready response, is a line consisting of a "+" character followed by a single space and a BASE64 encoded string. The client answer consists of a line containing a BASE64 encoded string. If the client wishes to cancel an authentication exchange, it should issue a line with a single "*". If the server receives such an answer, it must reject the AUTH command by sending a negative response.
546+
///
547+
/// A protection mechanism provides integrity and privacy protection to the protocol session. If a protection mechanism is negotiated, it is applied to all subsequent data sent over the connection. The protection mechanism takes effect immediately following the CRLF that concludes the authentication exchange for the client, and the CRLF of the positive response for the server. Once the protection mechanism is in effect, the stream of command and response octets is processed into buffers of ciphertext. Each buffer is transferred over the connection as a stream of octets prepended with a four octet field in network byte order that represents the length of the following data. The maximum ciphertext buffer length is defined by the protection mechanism.
548+
///
549+
/// The server is not required to support any particular authentication mechanism, nor are authentication mechanisms required to support any protection mechanisms. If an AUTH command fails with a negative response, the session remains in the AUTHORIZATION state and client may try another authentication mechanism by issuing another AUTH command, or may attempt to authenticate by using the USER/PASS or APOP commands. In other words, the client may request authentication types in decreasing order of preference, with the USER/PASS or APOP command as a last resort.
550+
#[cfg(feature = "sasl")]
551+
pub async fn auth<A: sasl::Authenticator + Sync>(&mut self, authenticator: A) -> Result<Text> {
552+
self.check_client_state(ClientState::Authentication)?;
553+
554+
self.has_read_greeting()?;
555+
556+
let mut request: Request = Auth.into();
557+
558+
let mechanism = authenticator.mechanism();
559+
560+
request.add_arg(mechanism);
538561

539-
// self.has_read_greeting()?;
562+
if let Some(arg) = authenticator.auth() {
563+
request.add_arg(crate::base64::encode(arg))
564+
}
540565

541-
// let mut request: Request = Auth.into();
566+
let stream = self.inner_mut()?;
542567

543-
// request.add_arg(auth_type.as_ref());
568+
stream.encode(&request).await?;
544569

545-
// let response = self.send_request(request).await?;
570+
let communicator = sasl::Communicator::new(stream);
546571

547-
// self.state = ClientState::Transaction;
572+
authenticator.handle(communicator).await?;
548573

549-
// match response {
550-
// Response::Message(resp) => Ok(resp),
551-
// _ => err!(
552-
// ErrorKind::UnexpectedResponse,
553-
// "Did not received the expected auth response"
554-
// ),
555-
// }
556-
// }
574+
let message = match stream.read_response(request).await? {
575+
Response::Message(message) => message,
576+
_ => err!(
577+
ErrorKind::UnexpectedResponse,
578+
"Did not received the expected auith response"
579+
),
580+
};
581+
582+
self.update_capabilities().await;
583+
584+
self.state = ClientState::Transaction;
585+
586+
Ok(message)
587+
}
557588

558589
/// ## USER & PASS
559590
///

src/response/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub enum Response {
3737
Uidl(UidlResponse),
3838
Capability(Vec<Capability>),
3939
Message(Text),
40+
#[cfg(feature = "sasl")]
41+
Challenge(Text),
4042
Err(Text),
4143
}
4244

src/response/parser/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
mod core;
2+
#[cfg(feature = "sasl")]
3+
mod rfc1734;
24
mod rfc1939;
35
mod rfc2449;
46

@@ -21,6 +23,20 @@ pub(crate) fn parse<'a>(input: &'a [u8], request: &Command) -> IResult<&'a [u8],
2123
return Err(nom::Err::Incomplete(nom::Needed::Unknown));
2224
}
2325

26+
#[cfg(feature = "sasl")]
27+
match request {
28+
Command::Base64(_) => match rfc1734::auth(input) {
29+
Ok((input, base64_challenge)) => {
30+
if let Ok(challenge) = crate::base64::decode(base64_challenge) {
31+
return Ok((input, Response::Challenge(challenge.into())));
32+
}
33+
}
34+
Err(nom::Err::Incomplete(needed)) => return Err(nom::Err::Incomplete(needed)),
35+
Err(_) => {}
36+
},
37+
_ => {}
38+
}
39+
2440
let (input, status) = status(input)?;
2541

2642
if status.success() {

src/response/parser/rfc1734.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use nom::{bytes::streaming::tag, character::streaming::space1, IResult};
2+
3+
use super::core::message_parser;
4+
5+
pub(crate) fn auth<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
6+
let (input, _) = tag("+")(input)?;
7+
let (input, _) = space1(input)?;
8+
let (input, content) = message_parser(input)?;
9+
10+
Ok((input, content.unwrap_or(b"")))
11+
}

0 commit comments

Comments
 (0)