do our own line buffering in preparation for starttls

This also allows us to handle excessive input line length as well.
This commit is contained in:
Wez Furlong
2023-02-16 22:20:44 -07:00
parent 7410cfd1b1
commit 03fb1f1cb8
4 changed files with 92 additions and 24 deletions
+84 -21
View File
@@ -10,7 +10,7 @@ use std::fmt::Debug;
use std::net::SocketAddr;
use std::sync::Arc;
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter, ReadHalf, WriteHalf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{error, instrument};
#[derive(Error, Debug, Clone)]
@@ -51,14 +51,15 @@ impl RejectError {
#[derive(Debug)]
pub struct SmtpServer {
reader: BufReader<ReadHalf<BoxedAsyncReadAndWrite>>,
writer: BufWriter<WriteHalf<BoxedAsyncReadAndWrite>>,
socket: BoxedAsyncReadAndWrite,
state: Option<TransactionState>,
said_hello: Option<String>,
config: LuaConfig,
hostname: String,
peer_address: SocketAddr,
relay_hosts: Vec<IpCidr>,
tls_active: bool,
read_buffer: Vec<u8>,
}
#[derive(Debug)]
@@ -80,18 +81,16 @@ impl SmtpServer {
{
let socket: BoxedAsyncReadAndWrite = Box::new(socket);
let config = load_config().await?;
let (reader, writer) = tokio::io::split(socket);
let reader = tokio::io::BufReader::new(reader);
let writer = tokio::io::BufWriter::new(writer);
let mut server = SmtpServer {
reader,
writer,
socket,
state: None,
said_hello: None,
config,
hostname,
peer_address,
relay_hosts,
tls_active: false,
read_buffer: Vec::with_capacity(1024),
};
tokio::task::spawn_local(async move {
@@ -116,16 +115,41 @@ impl SmtpServer {
let is_last = lines.peek().is_none();
let sep = if is_last { ' ' } else { '-' };
let text = format!("{status}{sep}{line}\r\n");
self.writer.write(text.as_bytes()).await?;
self.socket.write(text.as_bytes()).await?;
}
self.writer.flush().await?;
self.socket.flush().await?;
Ok(())
}
async fn read_line(&mut self) -> anyhow::Result<String> {
let mut line = String::new();
self.reader.read_line(&mut line).await?;
Ok(line)
async fn read_line(&mut self) -> anyhow::Result<ReadLine> {
let mut too_long = false;
loop {
let mut iter = self.read_buffer.iter().enumerate();
while let Some((i, &b)) = iter.next() {
if b != b'\r' {
continue;
}
if let Some((_, b'\n')) = iter.next() {
if too_long {
self.read_buffer.drain(0..i + 2);
return Ok(ReadLine::TooLong);
}
let line = String::from_utf8(self.read_buffer[0..i].to_vec());
self.read_buffer.drain(0..i + 2);
return Ok(ReadLine::Line(line?));
}
}
if self.read_buffer.len() > MAX_LINE_LEN {
self.read_buffer.clear();
too_long = true;
}
// Didn't find a complete line, fill up the rest of the buffer
let mut data = [0u8; 1024];
let size = self.socket.read(&mut data).await?;
self.read_buffer.extend_from_slice(&data[0..size]);
}
}
pub async fn call_callback<'lua, S: AsRef<str>, A: ToLuaMulti<'lua> + Clone>(
@@ -161,10 +185,15 @@ impl SmtpServer {
self.write_response(220, format!("{} KumoMTA\nW00t!\nYeah!", self.hostname))
.await?;
loop {
let line = self.read_line().await?;
let line = line.trim_end();
let line = match self.read_line().await? {
ReadLine::Line(line) => line,
ReadLine::TooLong => {
self.write_response(500, "5.2.3 line too long").await?;
continue;
}
};
match Command::parse(line) {
match Command::parse(&line) {
Err(err) => {
self.write_response(
501,
@@ -177,6 +206,14 @@ impl SmtpServer {
.await?;
return Ok(());
}
Ok(Command::StartTls) => {
if self.tls_active {
self.write_response(501, "Cannot STARTTLS as TLS is already active")
.await?;
continue;
}
anyhow::bail!("FIXME: implement STARTTLS");
}
Ok(Command::Ehlo(domain)) => {
let domain = domain.to_string();
@@ -188,11 +225,17 @@ impl SmtpServer {
continue;
}
let mut extensions = vec!["PIPELINING", "ENHANCEDSTATUSCODES"];
if !self.tls_active {
extensions.push("STARTTLS");
}
self.write_response(
250,
format!(
"{} Aloha {domain}\nPIPELINING\nENHANCEDSTATUSCODES",
self.hostname
"{} Aloha {domain}\n{}",
self.hostname,
extensions.join("\n"),
),
)
.await?;
@@ -299,10 +342,18 @@ impl SmtpServer {
.await?;
let mut data = vec![];
let mut too_long = false;
loop {
let line = self.read_line().await?;
if line == ".\r\n" {
let line = match self.read_line().await? {
ReadLine::Line(line) => line,
ReadLine::TooLong => {
too_long = true;
data.clear();
continue;
}
};
if line == "." {
break;
}
@@ -315,6 +366,11 @@ impl SmtpServer {
data.extend_from_slice(line.as_bytes());
}
if too_long {
self.write_response(500, "5.2.3 line too long").await?;
continue;
}
let state = self
.state
.take()
@@ -389,3 +445,10 @@ impl SmtpServer {
}
}
}
const MAX_LINE_LEN: usize = 998;
#[derive(PartialEq)]
enum ReadLine {
Line(String),
TooLong,
}
+2
View File
@@ -21,6 +21,7 @@ impl Parser {
Rule::data => Ok(Command::Data),
Rule::rset => Ok(Command::Rset),
Rule::quit => Ok(Command::Quit),
Rule::starttls => Ok(Command::StartTls),
Rule::vrfy => Self::parse_vrfy(result.into_inner()),
Rule::expn => Self::parse_expn(result.into_inner()),
Rule::help => Self::parse_help(result.into_inner()),
@@ -268,6 +269,7 @@ pub enum Command {
Expn(String),
Help(Option<String>),
Noop(Option<String>),
StartTls,
}
impl Command {
+2 -1
View File
@@ -66,5 +66,6 @@ vrfy = { ^"VRFY " ~ string }
expn = { ^"EXPN " ~ string }
help = { ^"HELP" ~ (" " ~ string)? }
noop = { ^"NOOP" ~ (" " ~ string)? }
starttls = { ^"STARTTLS" }
command = _{ SOI ~ mail | rcpt | ehlo | helo | data | rset | vrfy | expn | help | noop | quit ~ EOI }
command = _{ SOI ~ mail | rcpt | ehlo | helo | data | rset | vrfy | expn | help | noop | quit | starttls ~ EOI }
+4 -2
View File
@@ -1,10 +1,12 @@
use std::fmt::Debug;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio_rustls::client::TlsStream;
use tokio_rustls::client::TlsStream as TlsClientStream;
use tokio_rustls::server::TlsStream as TlsServerStream;
pub trait AsyncReadAndWrite: AsyncRead + AsyncWrite + Debug + Unpin + Send {}
impl AsyncReadAndWrite for TlsStream<TcpStream> {}
impl AsyncReadAndWrite for TlsClientStream<TcpStream> {}
impl AsyncReadAndWrite for TlsServerStream<TcpStream> {}
impl AsyncReadAndWrite for TcpStream {}
pub type BoxedAsyncReadAndWrite = Box<dyn AsyncReadAndWrite>;