Initial commit.

This commit is contained in:
Jill 2022-05-09 19:15:52 +02:00
commit 28ded269ea
No known key found for this signature in database
GPG key ID: 09A5A2688F13FAC1
16 changed files with 631 additions and 0 deletions

5
.editorconfig Normal file
View file

@ -0,0 +1,5 @@
root = true
[*.rs]
indent_style = space
indent_size = 4

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Rust stuff
/target
/Cargo.lock
# Misc stuff
.*
!.editorconfig
!.gitignore

25
Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "boitalettres"
version = "0.0.1"
license = "BSD-3-Clause"
edition = "2021"
[dependencies]
bytes = "1.1"
thiserror = "1.0"
tracing = "0.1"
# IMAP
imap-codec = "0.5"
# Async
async-compat = "0.2"
futures = "0.3"
pin-project = "1.0"
tokio = { version = "1.18", features = ["full"] }
tokio-tower = "0.6"
tower = { version = "0.4", features = ["full"] }
[dev-dependencies]
eyre = "0.6"
tracing-subscriber = "0.3"

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright (c) 2022 KokaKiwi <kokakiwi@deuxfleurs.fr>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of its contributors may
be used to endorse or promote products derived from this software
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

32
examples/simple.rs Normal file
View file

@ -0,0 +1,32 @@
use boitalettres::proto::{Request, Response};
use boitalettres::server::accept::addr::{AddrIncoming, AddrStream};
use boitalettres::server::Server;
async fn handle_req(req: Request) -> eyre::Result<Response> {
use imap_codec::types::response::Status;
tracing::debug!("Got request: {:#?}", req);
Ok(Response::Status(
Status::ok(Some(req.tag), None, "Ok").map_err(|e| eyre::eyre!(e))?,
))
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.init();
let incoming = AddrIncoming::new("127.0.0.1:4567").await?;
let make_service = tower::service_fn(|addr: &AddrStream| {
tracing::debug!("Accept: {} -> {}", addr.remote_addr, addr.local_addr);
futures::future::ok::<_, std::convert::Infallible>(tower::service_fn(handle_req))
});
let server = Server::new(incoming).serve(make_service);
let _ = server.await;
Ok(())
}

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["rustc-dev", "rust-src"]

7
scripts/test_imap.py Normal file
View file

@ -0,0 +1,7 @@
#!/usr/bin/python
from imaplib import IMAP4
conn = IMAP4('127.0.0.1', port=4567)
conn.noop()
conn.logout()

2
src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod proto;
pub mod server;

5
src/proto/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub use self::req::Request;
pub use self::res::Response;
pub mod req;
pub mod res;

3
src/proto/req.rs Normal file
View file

@ -0,0 +1,3 @@
use imap_codec::types::command::Command;
pub type Request = Command;

1
src/proto/res.rs Normal file
View file

@ -0,0 +1 @@
pub type Response = imap_codec::types::response::Response;

83
src/server/accept/addr.rs Normal file
View file

@ -0,0 +1,83 @@
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{self, Poll};
use async_compat::Compat as AsyncCompat;
use futures::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
use super::Accept;
#[pin_project::pin_project]
pub struct AddrIncoming {
pub local_addr: SocketAddr,
#[pin]
listener: TcpListener,
}
impl AddrIncoming {
pub async fn new(addr: impl ToSocketAddrs) -> std::io::Result<Self> {
let listener = TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
Ok(Self {
local_addr,
listener,
})
}
}
impl Accept for AddrIncoming {
type Conn = AddrStream;
type Error = std::io::Error;
fn poll_accept(
self: Pin<&mut Self>,
cx: &mut task::Context,
) -> task::Poll<Option<Result<Self::Conn, Self::Error>>> {
let this = self.project();
let (stream, remote_addr) = futures::ready!(this.listener.poll_accept(cx))?;
Poll::Ready(Some(Ok(AddrStream {
local_addr: *this.local_addr,
remote_addr,
stream: AsyncCompat::new(stream),
})))
}
}
#[pin_project::pin_project]
pub struct AddrStream {
pub local_addr: SocketAddr,
pub remote_addr: SocketAddr,
#[pin]
stream: AsyncCompat<TcpStream>,
}
impl AsyncRead for AddrStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
self.project().stream.poll_read(cx, buf)
}
}
impl AsyncWrite for AddrStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.project().stream.poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<std::io::Result<()>> {
self.project().stream.poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<std::io::Result<()>> {
self.project().stream.poll_close(cx)
}
}

79
src/server/accept/mod.rs Normal file
View file

@ -0,0 +1,79 @@
use std::error::Error as StdError;
use std::pin::Pin;
use std::task;
use futures::io;
use futures::Stream;
pub mod addr;
pub trait Accept {
type Conn: io::AsyncRead + io::AsyncWrite;
type Error: StdError;
fn poll_accept(
self: Pin<&mut Self>,
cx: &mut task::Context,
) -> task::Poll<Option<Result<Self::Conn, Self::Error>>>;
}
pub fn poll_fn<F, IO, E>(f: F) -> impl Accept<Conn = IO, Error = E>
where
F: FnMut(&mut task::Context) -> task::Poll<Option<Result<IO, E>>>,
IO: io::AsyncRead + io::AsyncWrite,
E: StdError,
{
struct PollFn<F>(F);
impl<F> Unpin for PollFn<F> {}
impl<F, IO, E> Accept for PollFn<F>
where
F: FnMut(&mut task::Context) -> task::Poll<Option<Result<IO, E>>>,
IO: io::AsyncRead + io::AsyncWrite,
E: StdError,
{
type Conn = IO;
type Error = E;
fn poll_accept(
self: Pin<&mut Self>,
cx: &mut task::Context,
) -> task::Poll<Option<Result<Self::Conn, Self::Error>>> {
(self.get_mut().0)(cx)
}
}
PollFn(f)
}
pub fn from_stream<S, IO, E>(stream: S) -> impl Accept<Conn = IO, Error = E>
where
S: Stream<Item = Result<IO, E>>,
IO: io::AsyncRead + io::AsyncWrite,
E: StdError,
{
use pin_project::pin_project;
#[pin_project]
struct FromStream<S>(#[pin] S);
impl<S, IO, E> Accept for FromStream<S>
where
S: Stream<Item = Result<IO, E>>,
IO: io::AsyncRead + io::AsyncWrite,
E: StdError,
{
type Conn = IO;
type Error = E;
fn poll_accept(
self: Pin<&mut Self>,
cx: &mut task::Context,
) -> task::Poll<Option<Result<Self::Conn, Self::Error>>> {
self.project().0.poll_next(cx)
}
}
FromStream(stream)
}

168
src/server/mod.rs Normal file
View file

@ -0,0 +1,168 @@
use std::future::Future;
use std::pin::Pin;
use std::task::{self, Poll};
use futures::io::{AsyncRead, AsyncWrite};
use imap_codec::types::response::Capability;
use tower::Service;
use crate::proto::{Request, Response};
use accept::Accept;
pub use service::MakeServiceRef;
pub mod accept;
mod pipeline;
mod service;
#[derive(Debug, thiserror::Error)]
pub enum Error<A> {
Accept(#[source] A),
MakeService(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
}
#[derive(Debug, Default, Clone)]
pub struct Imap {
pub capabilities: Vec<Capability>,
}
#[pin_project::pin_project]
pub struct Server<I, S> {
#[pin]
incoming: I,
make_service: S,
protocol: Imap,
}
impl<I> Server<I, ()>
where
I: Accept,
{
#[allow(clippy::new_ret_no_self)]
pub fn new(incoming: I) -> Builder<I> {
Builder::new(incoming)
}
}
impl<I, S> Future for Server<I, S>
where
I: Accept,
I::Conn: AsyncRead + AsyncWrite + Unpin + Send + 'static,
S: MakeServiceRef<I::Conn, Request, Response = Response>,
S::MakeError: Into<Box<dyn std::error::Error + Send + Sync + 'static>> + std::fmt::Display,
S::Error: std::fmt::Display,
S::Future: Send + 'static,
{
type Output = Result<(), Error<I::Error>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
loop {
let this = self.as_mut().project();
if let Some(conn) = futures::ready!(this.incoming.poll_accept(cx)) {
let conn = conn.map_err(Error::Accept)?;
futures::ready!(this.make_service.poll_ready_ref(cx))
.map_err(Into::into)
.map_err(Error::MakeService)?;
let service_fut = this.make_service.make_service_ref(&conn);
tokio::task::spawn(Connecting {
conn,
service_fut,
protocol: this.protocol.clone(),
});
} else {
return Poll::Ready(Ok(()));
}
}
}
}
pub struct Builder<I: Accept> {
incoming: I,
protocol: Imap,
}
#[allow(clippy::needless_update)]
impl<I: Accept> Builder<I> {
pub fn new(incoming: I) -> Self {
Self {
incoming,
protocol: Default::default(),
}
}
pub fn capabilities(self, capabilities: impl IntoIterator<Item = Capability>) -> Self {
let protocol = Imap {
capabilities: capabilities.into_iter().collect(),
..self.protocol
};
Self { protocol, ..self }
}
pub fn serve<S>(self, make_service: S) -> Server<I, S>
where
S: MakeServiceRef<I::Conn, Request, Response = Response>,
{
Server {
incoming: self.incoming,
make_service,
protocol: self.protocol,
}
}
}
#[pin_project::pin_project]
struct Connecting<C, F> {
conn: C,
#[pin]
service_fut: F,
protocol: Imap,
}
impl<C, F, ME, S> Future for Connecting<C, F>
where
C: AsyncRead + AsyncWrite + Unpin,
F: Future<Output = std::result::Result<S, ME>>,
ME: std::fmt::Display,
S: Service<Request, Response = Response>,
S::Error: std::fmt::Display,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
use tokio_tower::pipeline::Server as PipelineServer;
use pipeline::Connection;
let this = self.project();
let service = match futures::ready!(this.service_fut.poll(cx)) {
Ok(service) => service,
Err(err) => {
tracing::debug!("Connection error: {}", err);
return Poll::Ready(());
}
};
let mut conn = Connection::new(this.conn);
// TODO: Properly handle server greeting
{
use imap_codec::types::response::{Response, Status};
let status = Status::ok(None, None, "Hello").unwrap();
let res = Response::Status(status);
conn.send(res).unwrap();
}
let mut server = PipelineServer::new(conn, service);
if let Err(err) = futures::ready!(Future::poll(Pin::new(&mut server), cx)) {
tracing::debug!("Connection error: {}", err);
}
Poll::Ready(())
}
}

136
src/server/pipeline.rs Normal file
View file

@ -0,0 +1,136 @@
use std::pin::Pin;
use std::task::{self, Poll};
use bytes::BytesMut;
use futures::io::{AsyncRead, AsyncWrite};
use futures::sink::Sink;
use futures::stream::Stream;
use crate::proto::{Request, Response};
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
#[pin_project::pin_project]
pub struct Connection<C> {
#[pin]
conn: C,
read_buf: BytesMut,
write_buf: BytesMut,
}
impl<C> Connection<C> {
pub fn new(conn: C) -> Self {
Self {
conn,
read_buf: BytesMut::new(),
write_buf: BytesMut::new(),
}
}
}
impl<C> Stream for Connection<C>
where
C: AsyncRead + Unpin,
{
type Item = Result<Request, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
use imap_codec::parse::command::command as parse_command;
let mut this = self.project();
loop {
let (input, command) = match parse_command(this.read_buf) {
Ok(res) => res,
Err(e) if e.is_incomplete() => {
let mut buf = [0u8; 256];
let read = futures::ready!(this.conn.as_mut().poll_read(cx, &mut buf))?;
let data = &buf[..read];
this.read_buf.extend(data);
continue;
}
Err(e) => {
return Poll::Ready(Some(Err(format!("Error: {:?}", e).into())));
}
};
tracing::debug!("Received: {:#?}", command);
*this.read_buf = input.into();
let req = command;
return Poll::Ready(Some(Ok(req)));
}
}
}
impl<C> Connection<C>
where
C: AsyncWrite,
{
fn poll_flush_buffer(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Result<(), Error>> {
use bytes::Buf;
let mut this = self.project();
while !this.write_buf.is_empty() {
let written = futures::ready!(this.conn.as_mut().poll_write(cx, this.write_buf))?;
this.write_buf.advance(written);
}
Poll::Ready(Ok(()))
}
pub(crate) fn send(&mut self, item: Response) -> Result<(), Error> {
use bytes::BufMut;
use imap_codec::codec::Encode;
let mut writer = BufMut::writer(&mut self.write_buf);
tracing::debug!("Sending: {:#?}", item);
item.encode(&mut writer)?;
Ok(())
}
}
impl<C> Sink<Response> for Connection<C>
where
C: AsyncWrite + Unpin,
{
type Error = Error;
fn poll_ready(
self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
futures::ready!(self.poll_flush_buffer(cx))?;
Poll::Ready(Ok(()))
}
fn start_send(self: Pin<&mut Self>, item: Response) -> Result<(), Self::Error> {
debug_assert!(self.write_buf.is_empty());
self.get_mut().send(item)?;
Ok(())
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
futures::ready!(self.as_mut().poll_flush_buffer(cx))?;
futures::ready!(self.project().conn.poll_flush(cx))?;
Poll::Ready(Ok(()))
}
fn poll_close(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> Poll<Result<(), Self::Error>> {
futures::ready!(self.as_mut().poll_flush_buffer(cx))?;
futures::ready!(self.project().conn.poll_close(cx))?;
Poll::Ready(Ok(()))
}
}

50
src/server/service.rs Normal file
View file

@ -0,0 +1,50 @@
use std::future::Future;
use std::task::Context;
use std::task::Poll;
use tower::Service;
pub trait MakeServiceRef<Target, Request>: self::sealed::Sealed<(Target, Request)> {
type Response;
type Error;
type Service: Service<Request, Response = Self::Response, Error = Self::Error>;
type MakeError;
type Future: Future<Output = Result<Self::Service, Self::MakeError>>;
fn poll_ready_ref(&mut self, cx: &mut Context) -> Poll<Result<(), Self::MakeError>>;
fn make_service_ref(&mut self, target: &Target) -> Self::Future;
}
impl<M, S, Target, Request> self::sealed::Sealed<(Target, Request)> for M
where
M: for<'a> Service<&'a Target, Response = S>,
S: Service<Request>,
{
}
impl<M, ME, MF, S, Target, Request> MakeServiceRef<Target, Request> for M
where
M: for<'a> Service<&'a Target, Response = S, Future = MF, Error = ME>,
MF: Future<Output = Result<S, ME>>,
S: Service<Request>,
{
type Response = S::Response;
type Error = S::Error;
type Service = S;
type MakeError = ME;
type Future = MF;
fn poll_ready_ref(&mut self, cx: &mut Context) -> Poll<Result<(), Self::MakeError>> {
self.poll_ready(cx)
}
fn make_service_ref(&mut self, target: &Target) -> Self::Future {
self.call(target)
}
}
mod sealed {
pub trait Sealed<T> {}
}