Basic parsing of Dovecot Client Commands
This commit is contained in:
parent
9afd2ea337
commit
f9d6c1c927
4 changed files with 186 additions and 16 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -54,6 +54,7 @@ dependencies = [
|
||||||
"ldap3",
|
"ldap3",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix",
|
||||||
|
"nom 7.1.3",
|
||||||
"rand",
|
"rand",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
|
|
|
@ -32,12 +32,13 @@ chrono = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||||
nix = { version = "0.27", features = ["signal"] }
|
nix = { version = "0.27", features = ["signal"] }
|
||||||
clap = { version = "3.1.18", features = ["derive", "env"] }
|
clap = { version = "3.1.18", features = ["derive", "env"] }
|
||||||
|
|
||||||
# serialization
|
# serialization & parsing
|
||||||
serde = "1.0.137"
|
serde = "1.0.137"
|
||||||
rmp-serde = "0.15"
|
rmp-serde = "0.15"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
nom = "7.1"
|
||||||
zstd = { version = "0.9", default-features = false }
|
zstd = { version = "0.9", default-features = false }
|
||||||
|
|
||||||
# cryptography & security
|
# cryptography & security
|
||||||
|
|
189
src/auth.rs
189
src/auth.rs
|
@ -1,7 +1,7 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Result, anyhow};
|
||||||
use futures::stream::{FuturesUnordered, StreamExt};
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
use tokio::io::BufStream;
|
use tokio::io::BufStream;
|
||||||
use tokio::io::AsyncBufReadExt;
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
@ -60,7 +60,7 @@ impl AuthServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||||
tracing::info!("SASL Authentication Protocol listening on {:#}", self.bind_addr);
|
tracing::info!("SASL Authentication Protocol listening on {:#}", self.bind_addr);
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ impl AuthServer {
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!("AUTH: accepted connection from {}", remote_addr);
|
tracing::info!("AUTH: accepted connection from {}", remote_addr);
|
||||||
let conn = tokio::spawn(NetLoop::new(socket).run());
|
let conn = tokio::spawn(NetLoop::new(socket).run_error());
|
||||||
|
|
||||||
|
|
||||||
connections.push(conn);
|
connections.push(conn);
|
||||||
|
@ -107,24 +107,39 @@ impl NetLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(self) -> Result<()> {
|
async fn run_error(self) {
|
||||||
let mut lines = self.stream.lines();
|
match self.run().await {
|
||||||
while let Some(line) = lines.next_line().await? {
|
Ok(()) => tracing::info!("Auth session succeeded"),
|
||||||
|
Err(e) => tracing::error!(err=?e, "Auth session failed"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
async fn run(mut self) -> Result<()> {
|
||||||
|
let mut buff: Vec<u8> = Vec::new();
|
||||||
|
loop {
|
||||||
|
buff.clear();
|
||||||
|
self.stream.read_until(b'\n', &mut buff).await?;
|
||||||
|
let (input, cmd) = client_command(&buff).map_err(|_| anyhow!("Unable to parse command"))?;
|
||||||
|
println!("input: {:?}, cmd: {:?}", input, cmd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
// -----------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// DOVECOT AUTH TYPES
|
||||||
|
//
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
enum Mechanism {
|
enum Mechanism {
|
||||||
Plain,
|
Plain,
|
||||||
Login,
|
Login,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum AuthOptions {
|
enum AuthOption {
|
||||||
/// Unique session ID. Mainly used for logging.
|
/// Unique session ID. Mainly used for logging.
|
||||||
Session(u64),
|
Session(u64),
|
||||||
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
|
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
|
||||||
|
@ -176,7 +191,7 @@ enum AuthOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ClientCommands {
|
enum ClientCommand {
|
||||||
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
||||||
Version {
|
Version {
|
||||||
major: u64,
|
major: u64,
|
||||||
|
@ -189,11 +204,11 @@ enum ClientCommands {
|
||||||
id: u64,
|
id: u64,
|
||||||
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
|
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
|
||||||
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
||||||
mechanism: Mechanism,
|
mech: Mechanism,
|
||||||
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
|
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
|
||||||
service: String,
|
service: String,
|
||||||
/// All the optional parameters
|
/// All the optional parameters
|
||||||
options: Vec<AuthOptions>,
|
options: Vec<AuthOption>,
|
||||||
|
|
||||||
},
|
},
|
||||||
Cont {
|
Cont {
|
||||||
|
@ -235,7 +250,7 @@ enum FailCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ServerCommands {
|
enum ServerCommand {
|
||||||
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
/// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
|
||||||
Version {
|
Version {
|
||||||
major: u64,
|
major: u64,
|
||||||
|
@ -273,3 +288,149 @@ enum ServerCommands {
|
||||||
parameters: Vec<u8>,
|
parameters: Vec<u8>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// DOVECOT AUTH DECODING
|
||||||
|
//
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
IResult,
|
||||||
|
branch::alt,
|
||||||
|
error::{ErrorKind, Error},
|
||||||
|
character::complete::{tab, u64},
|
||||||
|
bytes::complete::{tag, tag_no_case, take, take_while, take_while1},
|
||||||
|
multi::{many1, separated_list0},
|
||||||
|
combinator::{map, opt, recognize, value,},
|
||||||
|
sequence::{pair, preceded, tuple},
|
||||||
|
};
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||||
|
let mut parser = tuple((
|
||||||
|
tag_no_case(b"VERSION"),
|
||||||
|
tab,
|
||||||
|
u64,
|
||||||
|
tab,
|
||||||
|
u64
|
||||||
|
));
|
||||||
|
|
||||||
|
let (input, (_, _, major, _, minor)) = parser(input)?;
|
||||||
|
Ok((input, ClientCommand::Version { major, minor }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||||
|
preceded(
|
||||||
|
pair(tag_no_case(b"CPID"), tab),
|
||||||
|
map(u64, |v| ClientCommand::Cpid(v))
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> {
|
||||||
|
alt((
|
||||||
|
value(Mechanism::Plain, tag_no_case(b"PLAIN")),
|
||||||
|
value(Mechanism::Login, tag_no_case(b"LOGIN")),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_not_tab_or_esc_or_lf(c: u8) -> bool {
|
||||||
|
c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
|
||||||
|
preceded(tag(&[0x01]), take(1usize))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
|
||||||
|
recognize(many1(alt((
|
||||||
|
take_while1(is_not_tab_or_esc_or_lf),
|
||||||
|
is_esc
|
||||||
|
))))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
|
||||||
|
let (input, buf) = preceded(
|
||||||
|
tag_no_case("service="),
|
||||||
|
parameter
|
||||||
|
)(input)?;
|
||||||
|
|
||||||
|
std::str::from_utf8(buf)
|
||||||
|
.map(|v| (input, v.to_string()))
|
||||||
|
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
|
||||||
|
alt((
|
||||||
|
value(AuthOption::Debug, tag_no_case(b"debug")),
|
||||||
|
value(AuthOption::NoPenalty, tag_no_case(b"no-penalty")),
|
||||||
|
value(AuthOption::CertUsername, tag_no_case(b"cert_username")),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||||
|
let mut parser = tuple((
|
||||||
|
tag_no_case(b"AUTH"),
|
||||||
|
tab,
|
||||||
|
u64,
|
||||||
|
tab,
|
||||||
|
mechanism,
|
||||||
|
tab,
|
||||||
|
service,
|
||||||
|
map(
|
||||||
|
opt(preceded(tab, separated_list0(tab, auth_option))),
|
||||||
|
|o| o.unwrap_or(vec![])
|
||||||
|
),
|
||||||
|
));
|
||||||
|
let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
|
||||||
|
Ok((input, ClientCommand::Auth { id, mech, service, options }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_base64_core(c: u8) -> bool {
|
||||||
|
c >= 0x30 && c <= 0x39 // 0-9
|
||||||
|
|| c >= 0x41 && c <= 0x5a // A-Z
|
||||||
|
|| c >= 0x61 && c <= 0x7a // a-z
|
||||||
|
|| c == 0x2b // +
|
||||||
|
|| c == 0x2f // /
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_base64_pad(c: u8) -> bool {
|
||||||
|
c == 0x3d
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @FIXME Dovecot does not say if base64 content must be padded or not
|
||||||
|
fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||||
|
let mut parser = tuple((
|
||||||
|
tag_no_case(b"CONT"),
|
||||||
|
tab,
|
||||||
|
u64,
|
||||||
|
tab,
|
||||||
|
take_while1(is_base64_core),
|
||||||
|
take_while(is_base64_pad),
|
||||||
|
));
|
||||||
|
|
||||||
|
let (input, (_, _, id, _, b64, _)) = parser(input)?;
|
||||||
|
let data = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64).map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
|
||||||
|
Ok((input, ClientCommand::Cont { id, data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
|
||||||
|
alt((
|
||||||
|
version_command,
|
||||||
|
cpid_command,
|
||||||
|
auth_command,
|
||||||
|
cont_command,
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// DOVECOT AUTH ENCODING
|
||||||
|
//
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
|
@ -49,12 +49,13 @@ impl Server {
|
||||||
let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone()));
|
let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone()));
|
||||||
let imap_unsecure_server = config.imap_unsecure.map(|imap| imap::new_unsecure(imap, login.clone()));
|
let imap_unsecure_server = config.imap_unsecure.map(|imap| imap::new_unsecure(imap, login.clone()));
|
||||||
let imap_server = config.imap.map(|imap| imap::new(imap, login.clone())).transpose()?;
|
let imap_server = config.imap.map(|imap| imap::new(imap, login.clone())).transpose()?;
|
||||||
|
let auth_server = config.auth.map(|auth| auth::AuthServer::new(auth, login.clone()));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lmtp_server,
|
lmtp_server,
|
||||||
imap_unsecure_server,
|
imap_unsecure_server,
|
||||||
imap_server,
|
imap_server,
|
||||||
auth_server: None,
|
auth_server,
|
||||||
pid_file: config.pid,
|
pid_file: config.pid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -98,6 +99,12 @@ impl Server {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
Some(s) => s.run(exit_signal.clone()).await,
|
Some(s) => s.run(exit_signal.clone()).await,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
match self.auth_server {
|
||||||
|
None => Ok(()),
|
||||||
|
Some(a) => a.run(exit_signal.clone()).await,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue