4

A SMTP server should display a welcome message upon establishing connection (220 service ready) which is a signal for the client to start sending commands. This seems to be in conflict with the request-response paradigm of tokio-proto.

I can imagine protocols could be completely inverted such as server sending requests and client responses (deprecated TURN), but for the moment I'm only concerned with the welcome message upon connection, aka banner. After that the client request => server response would be upheld.

I keep trying to figure where to hook this in, but the bind_server, bind_transport are super cryptic to me. Do I need to implement the transport?

I have this in the decode method of the codec. The problem is the decode method is not called unless there is data available to decode which kind of makes sense. I would expect there to be some connection initialization method to hook into but I've found nothing.

fn decode(&mut self, buf: &mut BytesMut) -> Result {

    if !self.initialized {
        println!(
            "new connection from {:?} to {:?}",
            self.peer_addr,
            self.local_addr
        );

        self.requests.push(SmtpCommand::Connect {
            local_addr: self.local_addr,
            peer_addr: self.peer_addr,
        });

        self.initialized = true;
    }
    //... snip
    match self.requests.is_empty() {
        true => Ok(None),
        false => Ok(Some(self.requests.remove(0))),
    }
}

My work-in-progress study project is on GitHub and I've also opened an issue with tokio-proto.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Robert Cutajar
  • 3,181
  • 1
  • 30
  • 42

1 Answers1

0

Implementing my own stateful transport decorator (SmtpConnectTransport) did the trick. It will inject the given frame upon initialization. I imagine it could be made into a generic solution by taking the initframe type as a parameter. The codec in the end doesn't have to do anything out of ordinary then, besides parsing and serializing.

With a frame coming right upon connection, the service can generate the desired welcome message or banner. I've included the local and remote socket addresses in the SmtpCommand::Connect for the benefit of the service as it will be used for spam detection.

My hunch was correct, but working it out felt like rusty metal grinding indeed :D I'm happy now how this samotop is coming together. Here's some code:

use std::io;
use std::str;
use bytes::Bytes;
use model::response::SmtpReply;
use model::request::SmtpCommand;
use protocol::codec::SmtpCodec;
use tokio_proto::streaming::pipeline::{Frame, Transport, ServerProto};
use tokio_io::codec::Framed;
use futures::{Stream, Sink, StartSend, Poll, Async};
use protocol::parser::SmtpParser;
use protocol::writer::SmtpSerializer;

type Error = io::Error;
type CmdFrame = Frame<SmtpCommand, Bytes, Error>;
type RplFrame = Frame<SmtpReply, (), Error>;

pub struct SmtpProto;

impl<TIO: NetSocket + 'static> ServerProto<TIO> for SmtpProto {
    type Error = Error;
    type Request = SmtpCommand;
    type RequestBody = Bytes;
    type Response = SmtpReply;
    type ResponseBody = ();
    type Transport = SmtpConnectTransport<Framed<TIO, SmtpCodec<'static>>>;
    type BindTransport = io::Result<Self::Transport>;

    fn bind_transport(&self, io: TIO) -> Self::BindTransport {
        // save local and remote socket address so we can use it as the first frame
        let initframe = Frame::Message {
            body: false,
            message: SmtpCommand::Connect {
                local_addr: io.local_addr().ok(),
                peer_addr: io.peer_addr().ok(),
            },
        };
        let codec = SmtpCodec::new(
            SmtpParser::session_parser(),
            SmtpSerializer::answer_serializer(),
        );
        let upstream = io.framed(codec);
        let transport = SmtpConnectTransport::new(upstream, initframe);
        Ok(transport)
    }
}

pub struct SmtpConnectTransport<TT> {
    initframe: Option<CmdFrame>,
    upstream: TT,
}

impl<TT> SmtpConnectTransport<TT> {
    pub fn new(upstream: TT, initframe: CmdFrame) -> Self {
        Self {
            upstream,
            initframe: Some(initframe),
        }
    }
}

impl<TT> Stream for SmtpConnectTransport<TT>
where
    TT: 'static + Stream<Error = Error, Item = CmdFrame>,
{
    type Error = Error;
    type Item = CmdFrame;

    fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
        match self.initframe.take() {
            Some(frame) => {
                println!("transport initializing");
                Ok(Async::Ready(Some(frame)))
            }
            None => self.upstream.poll(),
        }
    }
}

impl<TT> Sink for SmtpConnectTransport<TT>
where
    TT: 'static + Sink<SinkError = Error, SinkItem = RplFrame>,
{
    type SinkError = Error;
    type SinkItem = RplFrame;

    fn start_send(&mut self, request: Self::SinkItem) -> StartSend<Self::SinkItem, io::Error> {
        self.upstream.start_send(request)
    }

    fn poll_complete(&mut self) -> Poll<(), io::Error> {
        self.upstream.poll_complete()
    }

    fn close(&mut self) -> Poll<(), io::Error> {
        self.upstream.close()
    }
}

impl<TT> Transport for SmtpConnectTransport<TT>
where
    TT: 'static,
    TT: Stream<Error = Error, Item = CmdFrame>,
    TT: Sink<SinkError = Error, SinkItem = RplFrame>,
{
}


pub trait NetSocket: AsyncRead + AsyncWrite {
    fn peer_addr(&self) -> Result<SocketAddr>;
    fn local_addr(&self) -> Result<SocketAddr>;
}

impl NetSocket for TcpStream {
    fn peer_addr(&self) -> Result<SocketAddr> {
        self.peer_addr()
    }
    fn local_addr(&self) -> Result<SocketAddr> {
        self.local_addr()
    }
}
Robert Cutajar
  • 3,181
  • 1
  • 30
  • 42