WIP refactor
This commit is contained in:
parent
bb9cb386b6
commit
1a43ce5ac7
80 changed files with 1668 additions and 2649 deletions
1761
Cargo.lock
generated
1761
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
41
Cargo.toml
41
Cargo.toml
|
@ -1,16 +1,28 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "aerogramme"
|
resolver = "2"
|
||||||
version = "0.3.0"
|
members = [
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
"aero-user",
|
||||||
edition = "2021"
|
"aero-bayou",
|
||||||
license = "EUPL-1.2"
|
"aero-sasl",
|
||||||
description = "A robust email server"
|
"aero-dav",
|
||||||
|
"aero-dav/fuzz",
|
||||||
|
# "aero-collections",
|
||||||
|
# "aero-proto",
|
||||||
|
# "aerogramme",
|
||||||
|
]
|
||||||
|
|
||||||
[lib]
|
default-members = ["aerogramme"]
|
||||||
name = "aerogramme"
|
|
||||||
path = "src/lib.rs"
|
[workspace.dependencies]
|
||||||
|
# internal crates
|
||||||
|
aero-user = { version = "0.3.0", path = "aero-user" }
|
||||||
|
aero-bayou = { version = "0.3.0", path = "aero-bayou" }
|
||||||
|
aero-sasl = { version = "0.3.0", path = "aero-sasl" }
|
||||||
|
aero-dav = { version = "0.3.0", path = "aero-dav" }
|
||||||
|
#aero-collections = { version = "0.3.0", path = "aero-collections" }
|
||||||
|
#aero-proto = { version = "0.3.0", path = "aero-proto" }
|
||||||
|
#aerogramme = { version = "0.3.0", path = "aerogramme" }
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# async runtime
|
# async runtime
|
||||||
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||||
tokio-util = { version = "0.7", features = [ "compat" ] }
|
tokio-util = { version = "0.7", features = [ "compat" ] }
|
||||||
|
@ -80,13 +92,6 @@ aws-sdk-s3 = "1"
|
||||||
aws-smithy-runtime = "1"
|
aws-smithy-runtime = "1"
|
||||||
aws-smithy-runtime-api = "1"
|
aws-smithy-runtime-api = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
||||||
imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
||||||
|
|
||||||
[[test]]
|
|
||||||
name = "behavior"
|
|
||||||
path = "tests/behavior.rs"
|
|
||||||
harness = false
|
|
||||||
|
|
17
aero-bayou/Cargo.toml
Normal file
17
aero-bayou/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "aero-bayou"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "EUPL-1.2"
|
||||||
|
description = "A simplified version of Bayou by Terry et al. (ACM SIGOPS 1995)"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aero-user.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod timestamp
|
||||||
|
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
@ -7,9 +9,10 @@ use rand::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{watch, Notify};
|
use tokio::sync::{watch, Notify};
|
||||||
|
|
||||||
use crate::cryptoblob::*;
|
use aero_foundations::cryptoblob::*;
|
||||||
use crate::login::Credentials;
|
use aero_foundations::login::Credentials;
|
||||||
use crate::storage;
|
use aero_foundations::storage;
|
||||||
|
|
||||||
use crate::timestamp::*;
|
use crate::timestamp::*;
|
||||||
|
|
||||||
const KEEP_STATE_EVERY: usize = 64;
|
const KEEP_STATE_EVERY: usize = 64;
|
|
@ -1,7 +1,8 @@
|
||||||
use rand::prelude::*;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use rand::prelude::*;
|
||||||
|
|
||||||
/// Returns milliseconds since UNIX Epoch
|
/// Returns milliseconds since UNIX Epoch
|
||||||
pub fn now_msec() -> u64 {
|
pub fn now_msec() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
1
aero-dav/.gitignore
vendored
Normal file
1
aero-dav/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
14
aero-dav/Cargo.toml
Normal file
14
aero-dav/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "aero-dav"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "EUPL-1.2"
|
||||||
|
description = "A partial and standalone implementation of the WebDAV protocol and its extensions (eg. CalDAV or CardDAV)"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quick-xml.workspace = true
|
||||||
|
http.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
futures.workspace = true
|
0
fuzz/.gitignore → aero-dav/fuzz/.gitignore
vendored
0
fuzz/.gitignore → aero-dav/fuzz/.gitignore
vendored
0
fuzz/Cargo.lock → aero-dav/fuzz/Cargo.lock
generated
0
fuzz/Cargo.lock → aero-dav/fuzz/Cargo.lock
generated
|
@ -8,17 +8,14 @@ edition = "2021"
|
||||||
cargo-fuzz = true
|
cargo-fuzz = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libfuzzer-sys = "0.4"
|
arbitrary = { version = "1", optional = true, features = ["derive"] }
|
||||||
|
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
|
||||||
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
|
||||||
quick-xml = { version = "0.31", features = ["async-tokio"] }
|
quick-xml = { version = "0.31", features = ["async-tokio"] }
|
||||||
|
|
||||||
[dependencies.aerogramme]
|
[dependencies.aero-dav]
|
||||||
path = ".."
|
path = ".."
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
|
||||||
imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "dav"
|
name = "dav"
|
||||||
path = "fuzz_targets/dav.rs"
|
path = "fuzz_targets/dav.rs"
|
126
aero-dav/fuzz/dav.dict
Normal file
126
aero-dav/fuzz/dav.dict
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
#
|
||||||
|
# AFL dictionary for XML
|
||||||
|
# ----------------------
|
||||||
|
#
|
||||||
|
# Several basic syntax elements and attributes, modeled on libxml2.
|
||||||
|
#
|
||||||
|
# Created by Michal Zalewski
|
||||||
|
#
|
||||||
|
|
||||||
|
attr_encoding=" encoding=\"1\""
|
||||||
|
attr_generic=" a=\"1\""
|
||||||
|
attr_href=" href=\"1\""
|
||||||
|
attr_standalone=" standalone=\"no\""
|
||||||
|
attr_version=" version=\"1\""
|
||||||
|
attr_xml_base=" xml:base=\"1\""
|
||||||
|
attr_xml_id=" xml:id=\"1\""
|
||||||
|
attr_xml_lang=" xml:lang=\"1\""
|
||||||
|
attr_xml_space=" xml:space=\"1\""
|
||||||
|
attr_xmlns=" xmlns=\"1\""
|
||||||
|
|
||||||
|
entity_builtin="<"
|
||||||
|
entity_decimal=""
|
||||||
|
entity_external="&a;"
|
||||||
|
entity_hex=""
|
||||||
|
|
||||||
|
string_any="ANY"
|
||||||
|
string_brackets="[]"
|
||||||
|
string_cdata="CDATA"
|
||||||
|
string_col_fallback=":fallback"
|
||||||
|
string_col_generic=":a"
|
||||||
|
string_col_include=":include"
|
||||||
|
string_dashes="--"
|
||||||
|
string_empty="EMPTY"
|
||||||
|
string_empty_dblquotes="\"\""
|
||||||
|
string_empty_quotes="''"
|
||||||
|
string_entities="ENTITIES"
|
||||||
|
string_entity="ENTITY"
|
||||||
|
string_fixed="#FIXED"
|
||||||
|
string_id="ID"
|
||||||
|
string_idref="IDREF"
|
||||||
|
string_idrefs="IDREFS"
|
||||||
|
string_implied="#IMPLIED"
|
||||||
|
string_nmtoken="NMTOKEN"
|
||||||
|
string_nmtokens="NMTOKENS"
|
||||||
|
string_notation="NOTATION"
|
||||||
|
string_parentheses="()"
|
||||||
|
string_pcdata="#PCDATA"
|
||||||
|
string_percent="%a"
|
||||||
|
string_public="PUBLIC"
|
||||||
|
string_required="#REQUIRED"
|
||||||
|
string_schema=":schema"
|
||||||
|
string_system="SYSTEM"
|
||||||
|
string_ucs4="UCS-4"
|
||||||
|
string_utf16="UTF-16"
|
||||||
|
string_utf8="UTF-8"
|
||||||
|
string_xmlns="xmlns:"
|
||||||
|
|
||||||
|
tag_attlist="<!ATTLIST"
|
||||||
|
tag_cdata="<![CDATA["
|
||||||
|
tag_close="</a>"
|
||||||
|
tag_doctype="<!DOCTYPE"
|
||||||
|
tag_element="<!ELEMENT"
|
||||||
|
tag_entity="<!ENTITY"
|
||||||
|
tag_ignore="<![IGNORE["
|
||||||
|
tag_include="<![INCLUDE["
|
||||||
|
tag_notation="<!NOTATION"
|
||||||
|
tag_open="<a>"
|
||||||
|
tag_open_close="<a />"
|
||||||
|
tag_open_exclamation="<!"
|
||||||
|
tag_open_q="<?"
|
||||||
|
tag_sq2_close="]]>"
|
||||||
|
tag_xml_q="<?xml?>"
|
||||||
|
|
||||||
|
"0"
|
||||||
|
"1"
|
||||||
|
"activelock"
|
||||||
|
"allprop"
|
||||||
|
"cannot-modify-protected-property"
|
||||||
|
"collection"
|
||||||
|
"creationdate"
|
||||||
|
"DAV:"
|
||||||
|
"depth"
|
||||||
|
"displayname"
|
||||||
|
"error"
|
||||||
|
"exclusive"
|
||||||
|
"getcontentlanguage"
|
||||||
|
"getcontentlength"
|
||||||
|
"getcontenttype"
|
||||||
|
"getetag"
|
||||||
|
"getlastmodified"
|
||||||
|
"href"
|
||||||
|
"include"
|
||||||
|
"Infinite"
|
||||||
|
"infinity"
|
||||||
|
"location"
|
||||||
|
"lockdiscovery"
|
||||||
|
"lockentry"
|
||||||
|
"lockinfo"
|
||||||
|
"lockroot"
|
||||||
|
"lockscope"
|
||||||
|
"locktoken"
|
||||||
|
"lock-token-matches-request-uri"
|
||||||
|
"lock-token-submitted"
|
||||||
|
"locktype"
|
||||||
|
"multistatus"
|
||||||
|
"no-conflicting-lock"
|
||||||
|
"no-external-entities"
|
||||||
|
"owner"
|
||||||
|
"preserved-live-properties"
|
||||||
|
"prop"
|
||||||
|
"propertyupdate"
|
||||||
|
"propfind"
|
||||||
|
"propfind-finite-depth"
|
||||||
|
"propname"
|
||||||
|
"propstat"
|
||||||
|
"remove"
|
||||||
|
"resourcetype"
|
||||||
|
"response"
|
||||||
|
"responsedescription"
|
||||||
|
"set"
|
||||||
|
"shared"
|
||||||
|
"status"
|
||||||
|
"supportedlock"
|
||||||
|
"text/html"
|
||||||
|
"timeout"
|
||||||
|
"write"
|
196
aero-dav/fuzz/fuzz_targets/dav.rs
Normal file
196
aero-dav/fuzz/fuzz_targets/dav.rs
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
use libfuzzer_sys::arbitrary;
|
||||||
|
use libfuzzer_sys::arbitrary::Arbitrary;
|
||||||
|
|
||||||
|
use aero_dav::{types, realization, xml};
|
||||||
|
use quick_xml::reader::NsReader;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
const tokens: [&str; 63] = [
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"activelock",
|
||||||
|
"allprop",
|
||||||
|
"encoding",
|
||||||
|
"utf-8",
|
||||||
|
"http://ns.example.com/boxschema/",
|
||||||
|
"HTTP/1.1 200 OK",
|
||||||
|
"1997-12-01T18:27:21-08:00",
|
||||||
|
"Mon, 12 Jan 1998 09:25:56 GMT",
|
||||||
|
"\"abcdef\"",
|
||||||
|
"cannot-modify-protected-property",
|
||||||
|
"collection",
|
||||||
|
"creationdate",
|
||||||
|
"DAV:",
|
||||||
|
"D",
|
||||||
|
"C",
|
||||||
|
"xmlns:D",
|
||||||
|
"depth",
|
||||||
|
"displayname",
|
||||||
|
"error",
|
||||||
|
"exclusive",
|
||||||
|
"getcontentlanguage",
|
||||||
|
"getcontentlength",
|
||||||
|
"getcontenttype",
|
||||||
|
"getetag",
|
||||||
|
"getlastmodified",
|
||||||
|
"href",
|
||||||
|
"include",
|
||||||
|
"Infinite",
|
||||||
|
"infinity",
|
||||||
|
"location",
|
||||||
|
"lockdiscovery",
|
||||||
|
"lockentry",
|
||||||
|
"lockinfo",
|
||||||
|
"lockroot",
|
||||||
|
"lockscope",
|
||||||
|
"locktoken",
|
||||||
|
"lock-token-matches-request-uri",
|
||||||
|
"lock-token-submitted",
|
||||||
|
"locktype",
|
||||||
|
"multistatus",
|
||||||
|
"no-conflicting-lock",
|
||||||
|
"no-external-entities",
|
||||||
|
"owner",
|
||||||
|
"preserved-live-properties",
|
||||||
|
"prop",
|
||||||
|
"propertyupdate",
|
||||||
|
"propfind",
|
||||||
|
"propfind-finite-depth",
|
||||||
|
"propname",
|
||||||
|
"propstat",
|
||||||
|
"remove",
|
||||||
|
"resourcetype",
|
||||||
|
"response",
|
||||||
|
"responsedescription",
|
||||||
|
"set",
|
||||||
|
"shared",
|
||||||
|
"status",
|
||||||
|
"supportedlock",
|
||||||
|
"text/html",
|
||||||
|
"timeout",
|
||||||
|
"write",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Arbitrary)]
|
||||||
|
enum Token {
|
||||||
|
Known(usize),
|
||||||
|
//Unknown(String),
|
||||||
|
}
|
||||||
|
impl Token {
|
||||||
|
fn serialize(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Known(i) => tokens[i % tokens.len()].to_string(),
|
||||||
|
//Self::Unknown(v) => v.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Arbitrary)]
|
||||||
|
struct Tag {
|
||||||
|
//prefix: Option<Token>,
|
||||||
|
name: Token,
|
||||||
|
attr: Option<(Token, Token)>,
|
||||||
|
}
|
||||||
|
impl Tag {
|
||||||
|
fn start(&self) -> String {
|
||||||
|
let mut acc = String::new();
|
||||||
|
/*if let Some(p) = &self.prefix {
|
||||||
|
acc.push_str(p.serialize().as_str());
|
||||||
|
acc.push_str(":");
|
||||||
|
}*/
|
||||||
|
acc.push_str("D:");
|
||||||
|
acc.push_str(self.name.serialize().as_str());
|
||||||
|
|
||||||
|
if let Some((k,v)) = &self.attr {
|
||||||
|
acc.push_str(" ");
|
||||||
|
acc.push_str(k.serialize().as_str());
|
||||||
|
acc.push_str("=\"");
|
||||||
|
acc.push_str(v.serialize().as_str());
|
||||||
|
acc.push_str("\"");
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
fn end(&self) -> String {
|
||||||
|
let mut acc = String::new();
|
||||||
|
acc.push_str("D:");
|
||||||
|
acc.push_str(self.name.serialize().as_str());
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Arbitrary)]
|
||||||
|
enum XmlNode {
|
||||||
|
Node(Tag, Vec<Self>),
|
||||||
|
Number(u64),
|
||||||
|
Text(Token),
|
||||||
|
}
|
||||||
|
impl std::fmt::Debug for XmlNode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.serialize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl XmlNode {
|
||||||
|
fn serialize(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Node(tag, children) => {
|
||||||
|
let stag = tag.start();
|
||||||
|
match children.is_empty() {
|
||||||
|
true => format!("<{}/>", stag),
|
||||||
|
false => format!("<{}>{}</{}>", stag, children.iter().map(|v| v.serialize()).collect::<String>(), tag.end()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Number(v) => format!("{}", v),
|
||||||
|
Self::Text(v) => v.serialize(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
|
||||||
|
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
|
||||||
|
let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()) ];
|
||||||
|
let mut writer = xml::Writer { q, ns_to_apply };
|
||||||
|
|
||||||
|
elem.qwrite(&mut writer).await.expect("xml serialization");
|
||||||
|
tokio_buffer.flush().await.expect("tokio buffer flush");
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
|
||||||
|
|
||||||
|
fuzz_target!(|nodes: XmlNode| {
|
||||||
|
let gen = format!("<D:multistatus xmlns:D=\"DAV:\">{}<D:/multistatus>", nodes.serialize());
|
||||||
|
//println!("--------\n{}", gen);
|
||||||
|
let data = gen.as_bytes();
|
||||||
|
|
||||||
|
let rt = Runtime::new().expect("tokio runtime initialization");
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
// 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
|
||||||
|
let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
|
||||||
|
Err(_) => return,
|
||||||
|
Ok(r) => r,
|
||||||
|
};
|
||||||
|
let reference = match rdr.find::<Object>().await {
|
||||||
|
Err(_) => return,
|
||||||
|
Ok(m) => m,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Re-serialize the input
|
||||||
|
let my_serialization = serialize(&reference).await;
|
||||||
|
|
||||||
|
// 3. De-serialize my serialization
|
||||||
|
let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice())).await.expect("XML Reader init");
|
||||||
|
let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
|
||||||
|
|
||||||
|
// 4. Both the first decoding and last decoding must be identical
|
||||||
|
assert_eq!(reference, comparison);
|
||||||
|
})
|
||||||
|
});
|
|
@ -665,8 +665,8 @@ impl QWrite for TimeRange {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::dav::types as dav;
|
use crate::types as dav;
|
||||||
use crate::dav::realization::Calendar;
|
use crate::realization::Calendar;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use chrono::{Utc,TimeZone,DateTime};
|
use chrono::{Utc,TimeZone,DateTime};
|
||||||
|
|
|
@ -26,7 +26,9 @@ use super::xml;
|
||||||
/// processing details can be found in the definition of the DAV:set
|
/// processing details can be found in the definition of the DAV:set
|
||||||
/// instruction in Section 12.13.2 of [RFC2518].
|
/// instruction in Section 12.13.2 of [RFC2518].
|
||||||
///
|
///
|
||||||
|
/// ```xmlschema
|
||||||
/// <!ELEMENT mkcalendar (DAV:set)>
|
/// <!ELEMENT mkcalendar (DAV:set)>
|
||||||
|
/// ```
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>);
|
pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>);
|
||||||
|
|
||||||
|
@ -197,12 +199,15 @@ pub enum Property {
|
||||||
/// sequence "]]>", which is the end delimiter for the CDATA section.
|
/// sequence "]]>", which is the end delimiter for the CDATA section.
|
||||||
///
|
///
|
||||||
/// Definition:
|
/// Definition:
|
||||||
///
|
///
|
||||||
|
/// ```xmlschema
|
||||||
/// <!ELEMENT calendar-timezone (#PCDATA)>
|
/// <!ELEMENT calendar-timezone (#PCDATA)>
|
||||||
/// PCDATA value: an iCalendar object with exactly one VTIMEZONE component.
|
/// PCDATA value: an iCalendar object with exactly one VTIMEZONE component.
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
///
|
///
|
||||||
|
/// ```xmlschema
|
||||||
/// <C:calendar-timezone
|
/// <C:calendar-timezone
|
||||||
/// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
|
/// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR
|
||||||
/// PRODID:-//Example Corp.//CalDAV Client//EN
|
/// PRODID:-//Example Corp.//CalDAV Client//EN
|
||||||
|
@ -227,6 +232,7 @@ pub enum Property {
|
||||||
/// END:VTIMEZONE
|
/// END:VTIMEZONE
|
||||||
/// END:VCALENDAR
|
/// END:VCALENDAR
|
||||||
/// </C:calendar-timezone>
|
/// </C:calendar-timezone>
|
||||||
|
/// ```
|
||||||
//@FIXME we might want to put a buffer here or an iCal parsed object
|
//@FIXME we might want to put a buffer here or an iCal parsed object
|
||||||
CalendarTimezone(String),
|
CalendarTimezone(String),
|
||||||
|
|
||||||
|
@ -1123,12 +1129,15 @@ pub enum CalendarSelector<E: dav::Extension> {
|
||||||
/// the targeted calendar component.
|
/// the targeted calendar component.
|
||||||
///
|
///
|
||||||
/// Definition:
|
/// Definition:
|
||||||
|
///
|
||||||
|
/// ```xmlschema
|
||||||
/// <!ELEMENT comp-filter (is-not-defined | (time-range?,
|
/// <!ELEMENT comp-filter (is-not-defined | (time-range?,
|
||||||
/// prop-filter*, comp-filter*))>
|
/// prop-filter*, comp-filter*))>
|
||||||
///
|
///
|
||||||
/// <!ATTLIST comp-filter name CDATA #REQUIRED>
|
/// <!ATTLIST comp-filter name CDATA #REQUIRED>
|
||||||
/// name value: a calendar object or calendar component
|
/// name value: a calendar object or calendar component
|
||||||
/// type (e.g., VEVENT)
|
/// type (e.g., VEVENT)
|
||||||
|
/// ```
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct CompFilter {
|
pub struct CompFilter {
|
||||||
pub name: Component,
|
pub name: Component,
|
||||||
|
@ -1187,12 +1196,14 @@ pub struct CompFilterMatch {
|
||||||
///
|
///
|
||||||
/// Definition:
|
/// Definition:
|
||||||
///
|
///
|
||||||
/// <!ELEMENT prop-filter (is-not-defined |
|
/// ```xmlschema
|
||||||
/// ((time-range | text-match)?,
|
/// <!ELEMENT prop-filter (is-not-defined |
|
||||||
/// param-filter*))>
|
/// ((time-range | text-match)?,
|
||||||
|
/// param-filter*))>
|
||||||
///
|
///
|
||||||
/// <!ATTLIST prop-filter name CDATA #REQUIRED>
|
/// <!ATTLIST prop-filter name CDATA #REQUIRED>
|
||||||
/// name value: a calendar property name (e.g., ATTENDEE)
|
/// name value: a calendar property name (e.g., ATTENDEE)
|
||||||
|
/// ```
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct PropFilter {
|
pub struct PropFilter {
|
||||||
pub name: Component,
|
pub name: Component,
|
||||||
|
@ -1278,10 +1289,12 @@ pub struct TextMatch {
|
||||||
///
|
///
|
||||||
/// Definition:
|
/// Definition:
|
||||||
///
|
///
|
||||||
|
/// ```xmlschema
|
||||||
/// <!ELEMENT param-filter (is-not-defined | text-match?)>
|
/// <!ELEMENT param-filter (is-not-defined | text-match?)>
|
||||||
///
|
///
|
||||||
/// <!ATTLIST param-filter name CDATA #REQUIRED>
|
/// <!ATTLIST param-filter name CDATA #REQUIRED>
|
||||||
/// name value: a property parameter name (e.g., PARTSTAT)
|
/// name value: a property parameter name (e.g., PARTSTAT)
|
||||||
|
/// ```
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct ParamFilter {
|
pub struct ParamFilter {
|
||||||
pub name: PropertyParameter,
|
pub name: PropertyParameter,
|
|
@ -1,7 +1,6 @@
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
use quick_xml::events::{Event, BytesStart, BytesDecl, BytesText};
|
use quick_xml::events::Event;
|
||||||
use quick_xml::events::attributes::AttrError;
|
use quick_xml::events::attributes::AttrError;
|
||||||
use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*};
|
use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*};
|
||||||
use quick_xml::reader::NsReader;
|
use quick_xml::reader::NsReader;
|
||||||
|
@ -603,7 +602,7 @@ impl QRead<Href> for Href {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::{FixedOffset, DateTime, TimeZone, Utc};
|
use chrono::{FixedOffset, DateTime, TimeZone, Utc};
|
||||||
use crate::dav::realization::Core;
|
use crate::realization::Core;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn basic_propfind_propname() {
|
async fn basic_propfind_propname() {
|
|
@ -1,10 +1,5 @@
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
use quick_xml::Error as QError;
|
use quick_xml::Error as QError;
|
||||||
use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{Event, BytesText};
|
||||||
use quick_xml::writer::ElementWriter;
|
|
||||||
use quick_xml::name::PrefixDeclaration;
|
|
||||||
use tokio::io::AsyncWrite;
|
|
||||||
use super::types::*;
|
use super::types::*;
|
||||||
use super::xml::{Node, Writer,QWrite,IWrite};
|
use super::xml::{Node, Writer,QWrite,IWrite};
|
||||||
|
|
||||||
|
@ -638,7 +633,7 @@ impl<E: Extension> QWrite for Violation<E> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::dav::realization::Core;
|
use crate::realization::Core;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
/// To run only the unit tests and avoid the behavior ones:
|
/// To run only the unit tests and avoid the behavior ones:
|
25
aero-dav/src/lib.rs
Normal file
25
aero-dav/src/lib.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#![feature(type_alias_impl_trait)]
|
||||||
|
#![feature(async_fn_in_trait)]
|
||||||
|
#![feature(async_closure)]
|
||||||
|
#![feature(trait_alias)]
|
||||||
|
|
||||||
|
// utils
|
||||||
|
pub mod error;
|
||||||
|
pub mod xml;
|
||||||
|
|
||||||
|
// webdav
|
||||||
|
pub mod types;
|
||||||
|
pub mod encoder;
|
||||||
|
pub mod decoder;
|
||||||
|
|
||||||
|
// calendar
|
||||||
|
pub mod caltypes;
|
||||||
|
pub mod calencoder;
|
||||||
|
pub mod caldecoder;
|
||||||
|
|
||||||
|
// wip
|
||||||
|
mod acltypes;
|
||||||
|
mod versioningtypes;
|
||||||
|
|
||||||
|
// final type
|
||||||
|
pub mod realization;
|
|
@ -3,7 +3,6 @@ use std::fmt::Debug;
|
||||||
|
|
||||||
use chrono::{DateTime,FixedOffset};
|
use chrono::{DateTime,FixedOffset};
|
||||||
use super::xml;
|
use super::xml;
|
||||||
use super::error;
|
|
||||||
|
|
||||||
/// It's how we implement a DAV extension
|
/// It's how we implement a DAV extension
|
||||||
/// (That's the dark magic part...)
|
/// (That's the dark magic part...)
|
|
@ -1,7 +1,8 @@
|
||||||
use tokio::io::{AsyncWrite, AsyncBufRead};
|
use futures::Future;
|
||||||
use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText};
|
use quick_xml::events::{Event, BytesStart};
|
||||||
use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*};
|
use quick_xml::name::ResolveResult;
|
||||||
use quick_xml::reader::NsReader;
|
use quick_xml::reader::NsReader;
|
||||||
|
use tokio::io::{AsyncWrite, AsyncBufRead};
|
||||||
|
|
||||||
use super::error::ParsingError;
|
use super::error::ParsingError;
|
||||||
|
|
||||||
|
@ -16,10 +17,10 @@ pub trait IRead = AsyncBufRead + Unpin;
|
||||||
|
|
||||||
// Serialization/Deserialization traits
|
// Serialization/Deserialization traits
|
||||||
pub trait QWrite {
|
pub trait QWrite {
|
||||||
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), quick_xml::Error>;
|
fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> impl Future<Output = Result<(), quick_xml::Error>>;
|
||||||
}
|
}
|
||||||
pub trait QRead<T> {
|
pub trait QRead<T> {
|
||||||
async fn qread(xml: &mut Reader<impl IRead>) -> Result<T, ParsingError>;
|
fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The representation of an XML node in Rust
|
// The representation of an XML node in Rust
|
|
@ -1,25 +1,3 @@
|
||||||
// utils
|
|
||||||
pub mod error;
|
|
||||||
pub mod xml;
|
|
||||||
|
|
||||||
// webdav
|
|
||||||
pub mod types;
|
|
||||||
pub mod encoder;
|
|
||||||
pub mod decoder;
|
|
||||||
|
|
||||||
// calendar
|
|
||||||
mod caltypes;
|
|
||||||
mod calencoder;
|
|
||||||
mod caldecoder;
|
|
||||||
|
|
||||||
// wip
|
|
||||||
mod acltypes;
|
|
||||||
mod versioningtypes;
|
|
||||||
|
|
||||||
// final type
|
|
||||||
pub mod realization;
|
|
||||||
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
140
aero-proto/sasl.rs
Normal file
140
aero-proto/sasl.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
|
use tokio::io::BufStream;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio::sync::watch;
|
||||||
|
|
||||||
|
use aero_user::config::AuthConfig;
|
||||||
|
use aero_user::login::ArcLoginProvider;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct AuthServer {
|
||||||
|
login_provider: ArcLoginProvider,
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthServer {
|
||||||
|
pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
|
||||||
|
Self {
|
||||||
|
bind_addr: config.bind_addr,
|
||||||
|
login_provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
||||||
|
let tcp = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
tracing::info!(
|
||||||
|
"SASL Authentication Protocol listening on {:#}",
|
||||||
|
self.bind_addr
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut connections = FuturesUnordered::new();
|
||||||
|
|
||||||
|
while !*must_exit.borrow() {
|
||||||
|
let wait_conn_finished = async {
|
||||||
|
if connections.is_empty() {
|
||||||
|
futures::future::pending().await
|
||||||
|
} else {
|
||||||
|
connections.next().await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (socket, remote_addr) = tokio::select! {
|
||||||
|
a = tcp.accept() => a?,
|
||||||
|
_ = wait_conn_finished => continue,
|
||||||
|
_ = must_exit.changed() => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("AUTH: accepted connection from {}", remote_addr);
|
||||||
|
let conn = tokio::spawn(
|
||||||
|
NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
|
||||||
|
);
|
||||||
|
|
||||||
|
connections.push(conn);
|
||||||
|
}
|
||||||
|
drop(tcp);
|
||||||
|
|
||||||
|
tracing::info!("AUTH server shutting down, draining remaining connections...");
|
||||||
|
while connections.next().await.is_some() {}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetLoop {
|
||||||
|
login: ArcLoginProvider,
|
||||||
|
stream: BufStream<TcpStream>,
|
||||||
|
stop: watch::Receiver<bool>,
|
||||||
|
state: State,
|
||||||
|
read_buf: Vec<u8>,
|
||||||
|
write_buf: BytesMut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetLoop {
|
||||||
|
fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
|
||||||
|
Self {
|
||||||
|
login,
|
||||||
|
stream: BufStream::new(stream),
|
||||||
|
state: State::Init,
|
||||||
|
stop,
|
||||||
|
read_buf: Vec::new(),
|
||||||
|
write_buf: BytesMut::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_error(self) {
|
||||||
|
match self.run().await {
|
||||||
|
Ok(()) => tracing::info!("Auth session succeeded"),
|
||||||
|
Err(e) => tracing::error!(err=?e, "Auth session failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(mut self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
|
||||||
|
// Detect EOF / socket close
|
||||||
|
let bread = read_res?;
|
||||||
|
if bread == 0 {
|
||||||
|
tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command
|
||||||
|
let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
|
||||||
|
tracing::trace!(cmd=?cmd, "Received command");
|
||||||
|
|
||||||
|
// Make some progress in our local state
|
||||||
|
self.state.progress(cmd, &self.login).await;
|
||||||
|
if matches!(self.state, State::Error) {
|
||||||
|
bail!("Internal state is in error, previous logs explain what went wrong");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
let srv_cmds = self.state.response();
|
||||||
|
srv_cmds.iter().try_for_each(|r| {
|
||||||
|
tracing::trace!(cmd=?r, "Sent command");
|
||||||
|
r.encode(&mut self.write_buf)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Send responses if at least one command response has been generated
|
||||||
|
if !srv_cmds.is_empty() {
|
||||||
|
self.stream.write_all(&self.write_buf).await?;
|
||||||
|
self.stream.flush().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset buffers
|
||||||
|
self.read_buf.clear();
|
||||||
|
self.write_buf.clear();
|
||||||
|
},
|
||||||
|
_ = self.stop.changed() => {
|
||||||
|
tracing::debug!("Server is stopping, quitting this runner");
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
aero-sasl/Cargo.toml
Normal file
22
aero-sasl/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "aero-sasl"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "EUPL-1.2"
|
||||||
|
description = "A partial and standalone implementation of the Dovecot SASL Auth Protocol"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
nom.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-util.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
|
||||||
|
#log.workspace = true
|
||||||
|
#serde.workspace = true
|
243
aero-sasl/src/decode.rs
Normal file
243
aero-sasl/src/decode.rs
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
use base64::Engine;
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{tag, tag_no_case, take, take_while, take_while1},
|
||||||
|
character::complete::{tab, u16, u64},
|
||||||
|
combinator::{map, opt, recognize, rest, value},
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
multi::{many1, separated_list0},
|
||||||
|
sequence::{pair, preceded, tuple},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
pub 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!();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
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(Version { major, minor })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 parameter_str(input: &[u8]) -> IResult<&[u8], String> {
|
||||||
|
let (input, buf) = parameter(input)?;
|
||||||
|
|
||||||
|
std::str::from_utf8(buf)
|
||||||
|
.map(|v| (input, v.to_string()))
|
||||||
|
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_param_name_char(c: u8) -> bool {
|
||||||
|
is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
|
||||||
|
let (input, buf) = take_while1(is_param_name_char)(input)?;
|
||||||
|
|
||||||
|
std::str::from_utf8(buf)
|
||||||
|
.map(|v| (input, v.to_string()))
|
||||||
|
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
|
||||||
|
preceded(tag_no_case("service="), parameter_str)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
|
||||||
|
use AuthOption::*;
|
||||||
|
alt((
|
||||||
|
alt((
|
||||||
|
value(Debug, tag_no_case(b"debug")),
|
||||||
|
value(NoPenalty, tag_no_case(b"no-penalty")),
|
||||||
|
value(ClientId, tag_no_case(b"client_id")),
|
||||||
|
value(NoLogin, tag_no_case(b"nologin")),
|
||||||
|
map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
|
||||||
|
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
|
||||||
|
LocalIp(ip)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
|
||||||
|
RemoteIp(ip)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"lport="), u16), |port| {
|
||||||
|
LocalPort(port)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"rport="), u16), |port| {
|
||||||
|
RemotePort(port)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
|
||||||
|
RealRemoteIp(ip)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
|
||||||
|
RealLocalIp(ip)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"real_lport="), u16), |port| {
|
||||||
|
RealLocalPort(port)
|
||||||
|
}),
|
||||||
|
map(preceded(tag_no_case(b"real_rport="), u16), |port| {
|
||||||
|
RealRemotePort(port)
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
alt((
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"local_name="), parameter_str),
|
||||||
|
|name| LocalName(name),
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"forward_views="), parameter),
|
||||||
|
|views| ForwardViews(views.into()),
|
||||||
|
),
|
||||||
|
map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
|
||||||
|
Secured(Some(info))
|
||||||
|
}),
|
||||||
|
value(Secured(None), tag_no_case(b"secured")),
|
||||||
|
value(CertUsername, tag_no_case(b"cert_username")),
|
||||||
|
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
|
||||||
|
Transport(ts)
|
||||||
|
}),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"tls_cipher="), parameter_str),
|
||||||
|
|cipher| TlsCipher(cipher),
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
|
||||||
|
|bits| TlsCipherBits(bits),
|
||||||
|
),
|
||||||
|
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
|
||||||
|
TlsPfs(pfs)
|
||||||
|
}),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"tls_protocol="), parameter_str),
|
||||||
|
|proto| TlsProtocol(proto),
|
||||||
|
),
|
||||||
|
map(
|
||||||
|
preceded(tag_no_case(b"valid-client-cert="), parameter_str),
|
||||||
|
|cert| ValidClientCert(cert),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
alt((
|
||||||
|
map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
|
||||||
|
map(
|
||||||
|
tuple((parameter_name, tag(b"="), parameter)),
|
||||||
|
|(n, _, v)| UnknownPair(n, v.into()),
|
||||||
|
),
|
||||||
|
map(parameter, |v| UnknownBool(v.into())),
|
||||||
|
)),
|
||||||
|
))(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 // =
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
||||||
|
let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
|
||||||
|
|
||||||
|
let data = base64::engine::general_purpose::STANDARD_NO_PAD
|
||||||
|
.decode(b64)
|
||||||
|
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
|
||||||
|
|
||||||
|
Ok((input, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @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, base64));
|
||||||
|
|
||||||
|
let (input, (_, _, id, _, data)) = parser(input)?;
|
||||||
|
Ok((input, ClientCommand::Cont { id, data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// SASL DECODING
|
||||||
|
//
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
fn not_null(c: u8) -> bool {
|
||||||
|
c != 0x0
|
||||||
|
}
|
||||||
|
|
||||||
|
// impersonated user, login, password
|
||||||
|
pub fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
|
||||||
|
map(
|
||||||
|
tuple((
|
||||||
|
take_while(not_null),
|
||||||
|
take(1usize),
|
||||||
|
take_while(not_null),
|
||||||
|
take(1usize),
|
||||||
|
rest,
|
||||||
|
)),
|
||||||
|
|(imp, _, user, _, pass)| (imp, user, pass),
|
||||||
|
)(input)
|
||||||
|
}
|
157
aero-sasl/src/encode.rs
Normal file
157
aero-sasl/src/encode.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use base64::Engine;
|
||||||
|
use tokio_util::bytes::{BufMut, BytesMut};
|
||||||
|
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
|
pub trait Encode {
|
||||||
|
fn encode(&self, out: &mut BytesMut) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_enc(out: &mut BytesMut) {
|
||||||
|
out.put(&[0x09][..])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lf_enc(out: &mut BytesMut) {
|
||||||
|
out.put(&[0x0A][..])
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for Mechanism {
|
||||||
|
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Plain => out.put(&b"PLAIN"[..]),
|
||||||
|
Self::Login => out.put(&b"LOGIN"[..]),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for MechanismParameters {
|
||||||
|
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Anonymous => out.put(&b"anonymous"[..]),
|
||||||
|
Self::PlainText => out.put(&b"plaintext"[..]),
|
||||||
|
Self::Dictionary => out.put(&b"dictionary"[..]),
|
||||||
|
Self::Active => out.put(&b"active"[..]),
|
||||||
|
Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
|
||||||
|
Self::MutualAuth => out.put(&b"mutual-auth"[..]),
|
||||||
|
Self::Private => out.put(&b"private"[..]),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for FailCode {
|
||||||
|
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::TempFail => out.put(&b"temp_fail"[..]),
|
||||||
|
Self::AuthzFail => out.put(&b"authz_fail"[..]),
|
||||||
|
Self::UserDisabled => out.put(&b"user_disabled"[..]),
|
||||||
|
Self::PassExpired => out.put(&b"pass_expired"[..]),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for ServerCommand {
|
||||||
|
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Version(Version { major, minor }) => {
|
||||||
|
out.put(&b"VERSION"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(major.to_string().as_bytes());
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(minor.to_string().as_bytes());
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Spid(pid) => {
|
||||||
|
out.put(&b"SPID"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(pid.to_string().as_bytes());
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Cuid(pid) => {
|
||||||
|
out.put(&b"CUID"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(pid.to_string().as_bytes());
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Cookie(cval) => {
|
||||||
|
out.put(&b"COOKIE"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(hex::encode(cval).as_bytes());
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Mech { kind, parameters } => {
|
||||||
|
out.put(&b"MECH"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
kind.encode(out)?;
|
||||||
|
for p in parameters.iter() {
|
||||||
|
tab_enc(out);
|
||||||
|
p.encode(out)?;
|
||||||
|
}
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Done => {
|
||||||
|
out.put(&b"DONE"[..]);
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Cont { id, data } => {
|
||||||
|
out.put(&b"CONT"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(id.to_string().as_bytes());
|
||||||
|
tab_enc(out);
|
||||||
|
if let Some(rdata) = data {
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
|
||||||
|
out.put(b64.as_bytes());
|
||||||
|
}
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Ok {
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
extra_parameters,
|
||||||
|
} => {
|
||||||
|
out.put(&b"OK"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(id.to_string().as_bytes());
|
||||||
|
if let Some(user) = user_id {
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(&b"user="[..]);
|
||||||
|
out.put(user.as_bytes());
|
||||||
|
}
|
||||||
|
for p in extra_parameters.iter() {
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(&p[..]);
|
||||||
|
}
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
Self::Fail {
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
code,
|
||||||
|
extra_parameters,
|
||||||
|
} => {
|
||||||
|
out.put(&b"FAIL"[..]);
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(id.to_string().as_bytes());
|
||||||
|
if let Some(user) = user_id {
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(&b"user="[..]);
|
||||||
|
out.put(user.as_bytes());
|
||||||
|
}
|
||||||
|
if let Some(code_val) = code {
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(&b"code="[..]);
|
||||||
|
code_val.encode(out)?;
|
||||||
|
}
|
||||||
|
for p in extra_parameters.iter() {
|
||||||
|
tab_enc(out);
|
||||||
|
out.put(&p[..]);
|
||||||
|
}
|
||||||
|
lf_enc(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
201
aero-sasl/src/flow.rs
Normal file
201
aero-sasl/src/flow.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
use futures::Future;
|
||||||
|
use rand::prelude::*;
|
||||||
|
|
||||||
|
use super::types::*;
|
||||||
|
use super::decode::auth_plain;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AuthRes {
|
||||||
|
Success(String),
|
||||||
|
Failed(Option<String>, Option<FailCode>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum State {
|
||||||
|
Error,
|
||||||
|
Init,
|
||||||
|
HandshakePart(Version),
|
||||||
|
HandshakeDone,
|
||||||
|
AuthPlainProgress { id: u64 },
|
||||||
|
AuthDone { id: u64, res: AuthRes },
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVER_MAJOR: u64 = 1;
|
||||||
|
const SERVER_MINOR: u64 = 2;
|
||||||
|
const EMPTY_AUTHZ: &[u8] = &[];
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::Init
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_auth_plain<'a, X, F>(&self, data: &'a [u8], login: X) -> AuthRes
|
||||||
|
where
|
||||||
|
X: FnOnce(&'a str, &'a str) -> F,
|
||||||
|
F: Future<Output=bool>,
|
||||||
|
{
|
||||||
|
// Check that we can extract user's login+pass
|
||||||
|
let (ubin, pbin) = match auth_plain(&data) {
|
||||||
|
Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::error!("Impersonating user is not supported");
|
||||||
|
return AuthRes::Failed(None, None);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
|
||||||
|
return AuthRes::Failed(None, None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to convert it to UTF-8
|
||||||
|
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
|
||||||
|
(Ok(u), Ok(p)) => (u, p),
|
||||||
|
_ => {
|
||||||
|
tracing::error!("Username or password contain invalid UTF-8 characters");
|
||||||
|
return AuthRes::Failed(None, None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to connect user
|
||||||
|
match login(user, password).await {
|
||||||
|
true => AuthRes::Success(user.to_string()),
|
||||||
|
false => {
|
||||||
|
tracing::warn!("login failed");
|
||||||
|
AuthRes::Failed(Some(user.to_string()), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn progress<F,X>(&mut self, cmd: ClientCommand, login: X)
|
||||||
|
where
|
||||||
|
X: FnOnce(&str, &str) -> F,
|
||||||
|
F: Future<Output=bool>,
|
||||||
|
{
|
||||||
|
let new_state = 'state: {
|
||||||
|
match (std::mem::replace(self, State::Error), cmd) {
|
||||||
|
(Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
|
||||||
|
(Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
|
||||||
|
if version.major != SERVER_MAJOR {
|
||||||
|
tracing::error!(
|
||||||
|
client_major = version.major,
|
||||||
|
server_major = SERVER_MAJOR,
|
||||||
|
"Unsupported client major version"
|
||||||
|
);
|
||||||
|
break 'state Self::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::HandshakeDone
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Self::HandshakeDone { .. },
|
||||||
|
ClientCommand::Auth {
|
||||||
|
id, mech, options, ..
|
||||||
|
},
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
Self::AuthDone { .. },
|
||||||
|
ClientCommand::Auth {
|
||||||
|
id, mech, options, ..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if mech != Mechanism::Plain {
|
||||||
|
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
|
||||||
|
break 'state Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: AuthRes::Failed(None, None),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match options.last() {
|
||||||
|
Some(AuthOption::Resp(data)) => Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: self.try_auth_plain(&data, login).await,
|
||||||
|
},
|
||||||
|
_ => Self::AuthPlainProgress { id },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
|
||||||
|
// Check that ID matches
|
||||||
|
if cid != id {
|
||||||
|
tracing::error!(
|
||||||
|
auth_id = id,
|
||||||
|
cont_id = cid,
|
||||||
|
"CONT id does not match AUTH id"
|
||||||
|
);
|
||||||
|
break 'state Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: AuthRes::Failed(None, None),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: self.try_auth_plain(&data, login).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::error!("This command is not valid in this context");
|
||||||
|
Self::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::debug!(state=?new_state, "Made progress");
|
||||||
|
*self = new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response(&self) -> Vec<ServerCommand> {
|
||||||
|
let mut srv_cmd: Vec<ServerCommand> = Vec::new();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::HandshakeDone { .. } => {
|
||||||
|
srv_cmd.push(ServerCommand::Version(Version {
|
||||||
|
major: SERVER_MAJOR,
|
||||||
|
minor: SERVER_MINOR,
|
||||||
|
}));
|
||||||
|
|
||||||
|
srv_cmd.push(ServerCommand::Mech {
|
||||||
|
kind: Mechanism::Plain,
|
||||||
|
parameters: vec![MechanismParameters::PlainText],
|
||||||
|
});
|
||||||
|
|
||||||
|
srv_cmd.push(ServerCommand::Spid(15u64));
|
||||||
|
srv_cmd.push(ServerCommand::Cuid(19350u64));
|
||||||
|
|
||||||
|
let mut cookie = [0u8; 16];
|
||||||
|
thread_rng().fill(&mut cookie);
|
||||||
|
srv_cmd.push(ServerCommand::Cookie(cookie));
|
||||||
|
|
||||||
|
srv_cmd.push(ServerCommand::Done);
|
||||||
|
}
|
||||||
|
Self::AuthPlainProgress { id } => {
|
||||||
|
srv_cmd.push(ServerCommand::Cont {
|
||||||
|
id: *id,
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: AuthRes::Success(user),
|
||||||
|
} => {
|
||||||
|
srv_cmd.push(ServerCommand::Ok {
|
||||||
|
id: *id,
|
||||||
|
user_id: Some(user.to_string()),
|
||||||
|
extra_parameters: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Self::AuthDone {
|
||||||
|
id,
|
||||||
|
res: AuthRes::Failed(maybe_user, maybe_failcode),
|
||||||
|
} => {
|
||||||
|
srv_cmd.push(ServerCommand::Fail {
|
||||||
|
id: *id,
|
||||||
|
user_id: maybe_user.clone(),
|
||||||
|
code: maybe_failcode.clone(),
|
||||||
|
extra_parameters: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
srv_cmd
|
||||||
|
}
|
||||||
|
}
|
43
aero-sasl/src/lib.rs
Normal file
43
aero-sasl/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/// Seek compatibility with the Dovecot Authentication Protocol
|
||||||
|
///
|
||||||
|
/// ## Trace
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// S: VERSION 1 2
|
||||||
|
/// S: MECH PLAIN plaintext
|
||||||
|
/// S: MECH LOGIN plaintext
|
||||||
|
/// S: SPID 15
|
||||||
|
/// S: CUID 17654
|
||||||
|
/// S: COOKIE f56692bee41f471ed01bd83520025305
|
||||||
|
/// S: DONE
|
||||||
|
/// C: VERSION 1 2
|
||||||
|
/// C: CPID 1
|
||||||
|
///
|
||||||
|
/// C: AUTH 2 PLAIN service=smtp
|
||||||
|
/// S: CONT 2
|
||||||
|
/// C: CONT 2 base64stringFollowingRFC4616==
|
||||||
|
/// S: OK 2 user=alice@example.tld
|
||||||
|
///
|
||||||
|
/// C: AUTH 42 LOGIN service=smtp
|
||||||
|
/// S: CONT 42 VXNlcm5hbWU6
|
||||||
|
/// C: CONT 42 b64User
|
||||||
|
/// S: CONT 42 UGFzc3dvcmQ6
|
||||||
|
/// C: CONT 42 b64Pass
|
||||||
|
/// S: FAIL 42 user=alice
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## RFC References
|
||||||
|
///
|
||||||
|
/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ## Dovecot References
|
||||||
|
///
|
||||||
|
/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
|
||||||
|
/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
||||||
|
/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
|
||||||
|
/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
|
||||||
|
pub mod types;
|
||||||
|
pub mod encode;
|
||||||
|
pub mod decode;
|
||||||
|
pub mod flow;
|
163
aero-sasl/src/types.rs
Normal file
163
aero-sasl/src/types.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Mechanism {
|
||||||
|
Plain,
|
||||||
|
Login,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AuthOption {
|
||||||
|
/// Unique session ID. Mainly used for logging.
|
||||||
|
Session(u64),
|
||||||
|
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
|
||||||
|
LocalIp(String),
|
||||||
|
/// Remote client IP
|
||||||
|
RemoteIp(String),
|
||||||
|
/// Local port connected to by the client.
|
||||||
|
LocalPort(u16),
|
||||||
|
/// Remote client port
|
||||||
|
RemotePort(u16),
|
||||||
|
/// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to.
|
||||||
|
RealRemoteIp(String),
|
||||||
|
RealLocalIp(String),
|
||||||
|
RealLocalPort(u16),
|
||||||
|
RealRemotePort(u16),
|
||||||
|
/// TLS SNI name
|
||||||
|
LocalName(String),
|
||||||
|
/// Enable debugging for this lookup.
|
||||||
|
Debug,
|
||||||
|
/// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
|
||||||
|
/// Note: we do not unescape the tabulation, and thus we don't parse the data
|
||||||
|
ForwardViews(Vec<u8>),
|
||||||
|
/// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
|
||||||
|
Secured(Option<String>),
|
||||||
|
/// The value can be “insecure”, “trusted” or “TLS”.
|
||||||
|
Transport(String),
|
||||||
|
/// TLS cipher being used.
|
||||||
|
TlsCipher(String),
|
||||||
|
/// The number of bits in the TLS cipher.
|
||||||
|
/// @FIXME: I don't know how if it's a string or an integer
|
||||||
|
TlsCipherBits(String),
|
||||||
|
/// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
|
||||||
|
TlsPfs(String),
|
||||||
|
/// TLS protocol name (e.g. SSLv3, TLSv1.2)
|
||||||
|
TlsProtocol(String),
|
||||||
|
/// Remote user has presented a valid SSL certificate.
|
||||||
|
ValidClientCert(String),
|
||||||
|
/// Ignore auth penalty tracking for this request
|
||||||
|
NoPenalty,
|
||||||
|
/// Unknown option sent by Postfix
|
||||||
|
NoLogin,
|
||||||
|
/// Username taken from client’s SSL certificate.
|
||||||
|
CertUsername,
|
||||||
|
/// IMAP ID string
|
||||||
|
ClientId,
|
||||||
|
/// An unknown key
|
||||||
|
UnknownPair(String, Vec<u8>),
|
||||||
|
UnknownBool(Vec<u8>),
|
||||||
|
/// Initial response for authentication mechanism.
|
||||||
|
/// NOTE: This must be the last parameter. Everything after it is ignored.
|
||||||
|
/// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
|
||||||
|
/// **This field is used when the data to pass is small, it's a way to "inline a continuation".
|
||||||
|
Resp(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Version {
|
||||||
|
pub major: u64,
|
||||||
|
pub minor: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub 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.
|
||||||
|
Version(Version),
|
||||||
|
/// CPID finishes the handshake from client.
|
||||||
|
Cpid(u64),
|
||||||
|
Auth {
|
||||||
|
/// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one.
|
||||||
|
id: u64,
|
||||||
|
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
|
||||||
|
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
||||||
|
mech: Mechanism,
|
||||||
|
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
|
||||||
|
service: String,
|
||||||
|
/// All the optional parameters
|
||||||
|
options: Vec<AuthOption>,
|
||||||
|
},
|
||||||
|
Cont {
|
||||||
|
/// The <id> must match the <id> of the AUTH command.
|
||||||
|
id: u64,
|
||||||
|
/// Data that will be serialized to / deserialized from base64
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MechanismParameters {
|
||||||
|
/// Anonymous authentication
|
||||||
|
Anonymous,
|
||||||
|
/// Transfers plaintext passwords
|
||||||
|
PlainText,
|
||||||
|
/// Subject to passive (dictionary) attack
|
||||||
|
Dictionary,
|
||||||
|
/// Subject to active (non-dictionary) attack
|
||||||
|
Active,
|
||||||
|
/// Provides forward secrecy between sessions
|
||||||
|
ForwardSecrecy,
|
||||||
|
/// Provides mutual authentication
|
||||||
|
MutualAuth,
|
||||||
|
/// Don’t advertise this as available SASL mechanism (eg. APOP)
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FailCode {
|
||||||
|
/// This is a temporary internal failure, e.g. connection was lost to SQL database.
|
||||||
|
TempFail,
|
||||||
|
/// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok).
|
||||||
|
AuthzFail,
|
||||||
|
/// User is disabled (password may or may not have been correct)
|
||||||
|
UserDisabled,
|
||||||
|
/// User’s password has expired.
|
||||||
|
PassExpired,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub 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.
|
||||||
|
Version(Version),
|
||||||
|
/// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
|
||||||
|
/// SPID can be used by authentication client to tell master which server process handled the authentication.
|
||||||
|
Spid(u64),
|
||||||
|
/// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server.
|
||||||
|
/// CUID is currently useful only for APOP authentication.
|
||||||
|
Cuid(u64),
|
||||||
|
Mech {
|
||||||
|
kind: Mechanism,
|
||||||
|
parameters: Vec<MechanismParameters>,
|
||||||
|
},
|
||||||
|
/// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
|
||||||
|
Cookie([u8; 16]),
|
||||||
|
/// DONE finishes the handshake from server.
|
||||||
|
Done,
|
||||||
|
|
||||||
|
Fail {
|
||||||
|
id: u64,
|
||||||
|
user_id: Option<String>,
|
||||||
|
code: Option<FailCode>,
|
||||||
|
extra_parameters: Vec<Vec<u8>>,
|
||||||
|
},
|
||||||
|
Cont {
|
||||||
|
id: u64,
|
||||||
|
data: Option<Vec<u8>>,
|
||||||
|
},
|
||||||
|
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
|
||||||
|
/// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
|
||||||
|
Ok {
|
||||||
|
id: u64,
|
||||||
|
user_id: Option<String>,
|
||||||
|
extra_parameters: Vec<Vec<u8>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
30
aero-user/Cargo.toml
Normal file
30
aero-user/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "aero-user"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "EUPL-1.2"
|
||||||
|
description = "Represent an encrypted user profile"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
zstd.workspace = true
|
||||||
|
sodiumoxide.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
ldap3.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
aws-config.workspace = true
|
||||||
|
aws-sdk-s3.workspace = true
|
||||||
|
aws-smithy-runtime.workspace = true
|
||||||
|
aws-smithy-runtime-api.workspace = true
|
||||||
|
hyper-rustls.workspace = true
|
||||||
|
hyper-util.workspace = true
|
||||||
|
k2v-client.workspace = true
|
||||||
|
rmp-serde.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
argon2.workspace = true
|
9
aero-user/src/lib.rs
Normal file
9
aero-user/src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pub mod config;
|
||||||
|
pub mod cryptoblob;
|
||||||
|
pub mod login;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
// A user is composed of 3 things:
|
||||||
|
// - An identity (login)
|
||||||
|
// - A storage profile (storage)
|
||||||
|
// - Some cryptography data (cryptoblob)
|
|
@ -1,11 +1,10 @@
|
||||||
use anyhow::Result;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::login::*;
|
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
pub struct LdapLoginProvider {
|
pub struct LdapLoginProvider {
|
||||||
ldap_server: String,
|
ldap_server: String,
|
|
@ -2,11 +2,11 @@ pub mod demo_provider;
|
||||||
pub mod ldap_provider;
|
pub mod ldap_provider;
|
||||||
pub mod static_provider;
|
pub mod static_provider;
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use base64::Engine;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
|
||||||
use crate::cryptoblob::*;
|
use crate::cryptoblob::*;
|
|
@ -1,12 +1,11 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use async_trait::async_trait;
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use crate::login::*;
|
use crate::login::*;
|
||||||
use crate::storage;
|
use crate::storage;
|
|
@ -6,7 +6,7 @@ use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
|
||||||
use hyper_util::rt::TokioExecutor;
|
use hyper_util::rt::TokioExecutor;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::storage::*;
|
use super::*;
|
||||||
|
|
||||||
pub struct GarageRoot {
|
pub struct GarageRoot {
|
||||||
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
|
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::storage::*;
|
use crate::storage::*;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::BTreeMap;
|
||||||
use std::ops::Bound::{self, Excluded, Included, Unbounded};
|
use std::ops::Bound::{self, Excluded, Included, Unbounded};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::RwLock;
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
/// This implementation is very inneficient, and not completely correct
|
/// This implementation is very inneficient, and not completely correct
|
|
@ -11,11 +11,12 @@
|
||||||
pub mod garage;
|
pub mod garage;
|
||||||
pub mod in_memory;
|
pub mod in_memory;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Alternative {
|
pub enum Alternative {
|
||||||
Tombstone,
|
Tombstone,
|
12
aerogramme/Cargo.toml
Normal file
12
aerogramme/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "aerogramme"
|
||||||
|
version = "0.3.0"
|
||||||
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "EUPL-1.2"
|
||||||
|
description = "A robust email server"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "behavior"
|
||||||
|
path = "tests/behavior.rs"
|
||||||
|
harness = false
|
|
@ -186,12 +186,12 @@
|
||||||
shell = gpkgs.mkShell {
|
shell = gpkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
cargo2nix.packages.x86_64-linux.default
|
cargo2nix.packages.x86_64-linux.default
|
||||||
fenix.packages.x86_64-linux.minimal.toolchain
|
fenix.packages.x86_64-linux.complete.toolchain
|
||||||
fenix.packages.x86_64-linux.rust-analyzer
|
#fenix.packages.x86_64-linux.rust-analyzer
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.minimal.rustc}"
|
echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.complete.toolchain}"
|
||||||
export RUST_SRC_PATH="${fenix.packages.x86_64-linux.latest.rust-src}/lib/rustlib/src/rust/library"
|
export RUST_SRC_PATH="${fenix.packages.x86_64-linux.complete.toolchain}/lib/rustlib/src/rust/library"
|
||||||
export RUST_ANALYZER_INTERNALS_DO_NOT_USE='this is unstable'
|
export RUST_ANALYZER_INTERNALS_DO_NOT_USE='this is unstable'
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
#![no_main]
|
|
||||||
|
|
||||||
use libfuzzer_sys::fuzz_target;
|
|
||||||
use aerogramme::dav::{types, realization, xml};
|
|
||||||
use quick_xml::reader::NsReader;
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
|
|
||||||
async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
|
|
||||||
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
|
|
||||||
let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()) ];
|
|
||||||
let mut writer = xml::Writer { q, ns_to_apply };
|
|
||||||
|
|
||||||
elem.qwrite(&mut writer).await.expect("xml serialization");
|
|
||||||
tokio_buffer.flush().await.expect("tokio buffer flush");
|
|
||||||
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
|
|
||||||
|
|
||||||
fuzz_target!(|data: &[u8]| {
|
|
||||||
let rt = Runtime::new().expect("tokio runtime initialization");
|
|
||||||
|
|
||||||
rt.block_on(async {
|
|
||||||
// 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
|
|
||||||
let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok(r) => r,
|
|
||||||
};
|
|
||||||
let reference = match rdr.find::<Object>().await {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok(m) => m,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Re-serialize the input
|
|
||||||
let my_serialization = serialize(&reference).await;
|
|
||||||
|
|
||||||
// 3. De-serialize my serialization
|
|
||||||
let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice())).await.expect("XML Reader init");
|
|
||||||
let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
|
|
||||||
|
|
||||||
// 4. Both the first decoding and last decoding must be identical
|
|
||||||
assert_eq!(reference, comparison);
|
|
||||||
})
|
|
||||||
});
|
|
941
src/auth.rs
941
src/auth.rs
|
@ -1,941 +0,0 @@
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
|
||||||
use futures::stream::{FuturesUnordered, StreamExt};
|
|
||||||
use tokio::io::BufStream;
|
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio::sync::watch;
|
|
||||||
|
|
||||||
use crate::config::AuthConfig;
|
|
||||||
use crate::login::ArcLoginProvider;
|
|
||||||
|
|
||||||
/// Seek compatibility with the Dovecot Authentication Protocol
|
|
||||||
///
|
|
||||||
/// ## Trace
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// S: VERSION 1 2
|
|
||||||
/// S: MECH PLAIN plaintext
|
|
||||||
/// S: MECH LOGIN plaintext
|
|
||||||
/// S: SPID 15
|
|
||||||
/// S: CUID 17654
|
|
||||||
/// S: COOKIE f56692bee41f471ed01bd83520025305
|
|
||||||
/// S: DONE
|
|
||||||
/// C: VERSION 1 2
|
|
||||||
/// C: CPID 1
|
|
||||||
///
|
|
||||||
/// C: AUTH 2 PLAIN service=smtp
|
|
||||||
/// S: CONT 2
|
|
||||||
/// C: CONT 2 base64stringFollowingRFC4616==
|
|
||||||
/// S: OK 2 user=alice@example.tld
|
|
||||||
///
|
|
||||||
/// C: AUTH 42 LOGIN service=smtp
|
|
||||||
/// S: CONT 42 VXNlcm5hbWU6
|
|
||||||
/// C: CONT 42 b64User
|
|
||||||
/// S: CONT 42 UGFzc3dvcmQ6
|
|
||||||
/// C: CONT 42 b64Pass
|
|
||||||
/// S: FAIL 42 user=alice
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## RFC References
|
|
||||||
///
|
|
||||||
/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ## Dovecot References
|
|
||||||
///
|
|
||||||
/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
|
|
||||||
/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
|
||||||
/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
|
|
||||||
/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
|
|
||||||
pub struct AuthServer {
|
|
||||||
login_provider: ArcLoginProvider,
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthServer {
|
|
||||||
pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
|
|
||||||
Self {
|
|
||||||
bind_addr: config.bind_addr,
|
|
||||||
login_provider,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
|
|
||||||
let tcp = TcpListener::bind(self.bind_addr).await?;
|
|
||||||
tracing::info!(
|
|
||||||
"SASL Authentication Protocol listening on {:#}",
|
|
||||||
self.bind_addr
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut connections = FuturesUnordered::new();
|
|
||||||
|
|
||||||
while !*must_exit.borrow() {
|
|
||||||
let wait_conn_finished = async {
|
|
||||||
if connections.is_empty() {
|
|
||||||
futures::future::pending().await
|
|
||||||
} else {
|
|
||||||
connections.next().await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (socket, remote_addr) = tokio::select! {
|
|
||||||
a = tcp.accept() => a?,
|
|
||||||
_ = wait_conn_finished => continue,
|
|
||||||
_ = must_exit.changed() => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!("AUTH: accepted connection from {}", remote_addr);
|
|
||||||
let conn = tokio::spawn(
|
|
||||||
NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
|
|
||||||
);
|
|
||||||
|
|
||||||
connections.push(conn);
|
|
||||||
}
|
|
||||||
drop(tcp);
|
|
||||||
|
|
||||||
tracing::info!("AUTH server shutting down, draining remaining connections...");
|
|
||||||
while connections.next().await.is_some() {}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NetLoop {
|
|
||||||
login: ArcLoginProvider,
|
|
||||||
stream: BufStream<TcpStream>,
|
|
||||||
stop: watch::Receiver<bool>,
|
|
||||||
state: State,
|
|
||||||
read_buf: Vec<u8>,
|
|
||||||
write_buf: BytesMut,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetLoop {
|
|
||||||
fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
|
|
||||||
Self {
|
|
||||||
login,
|
|
||||||
stream: BufStream::new(stream),
|
|
||||||
state: State::Init,
|
|
||||||
stop,
|
|
||||||
read_buf: Vec::new(),
|
|
||||||
write_buf: BytesMut::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_error(self) {
|
|
||||||
match self.run().await {
|
|
||||||
Ok(()) => tracing::info!("Auth session succeeded"),
|
|
||||||
Err(e) => tracing::error!(err=?e, "Auth session failed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(mut self) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
|
|
||||||
// Detect EOF / socket close
|
|
||||||
let bread = read_res?;
|
|
||||||
if bread == 0 {
|
|
||||||
tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse command
|
|
||||||
let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
|
|
||||||
tracing::trace!(cmd=?cmd, "Received command");
|
|
||||||
|
|
||||||
// Make some progress in our local state
|
|
||||||
self.state.progress(cmd, &self.login).await;
|
|
||||||
if matches!(self.state, State::Error) {
|
|
||||||
bail!("Internal state is in error, previous logs explain what went wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build response
|
|
||||||
let srv_cmds = self.state.response();
|
|
||||||
srv_cmds.iter().try_for_each(|r| {
|
|
||||||
tracing::trace!(cmd=?r, "Sent command");
|
|
||||||
r.encode(&mut self.write_buf)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Send responses if at least one command response has been generated
|
|
||||||
if !srv_cmds.is_empty() {
|
|
||||||
self.stream.write_all(&self.write_buf).await?;
|
|
||||||
self.stream.flush().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset buffers
|
|
||||||
self.read_buf.clear();
|
|
||||||
self.write_buf.clear();
|
|
||||||
},
|
|
||||||
_ = self.stop.changed() => {
|
|
||||||
tracing::debug!("Server is stopping, quitting this runner");
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// BUSINESS LOGIC
|
|
||||||
//
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
use rand::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum AuthRes {
|
|
||||||
Success(String),
|
|
||||||
Failed(Option<String>, Option<FailCode>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum State {
|
|
||||||
Error,
|
|
||||||
Init,
|
|
||||||
HandshakePart(Version),
|
|
||||||
HandshakeDone,
|
|
||||||
AuthPlainProgress { id: u64 },
|
|
||||||
AuthDone { id: u64, res: AuthRes },
|
|
||||||
}
|
|
||||||
|
|
||||||
const SERVER_MAJOR: u64 = 1;
|
|
||||||
const SERVER_MINOR: u64 = 2;
|
|
||||||
const EMPTY_AUTHZ: &[u8] = &[];
|
|
||||||
impl State {
|
|
||||||
async fn try_auth_plain<'a>(&self, data: &'a [u8], login: &ArcLoginProvider) -> AuthRes {
|
|
||||||
// Check that we can extract user's login+pass
|
|
||||||
let (ubin, pbin) = match auth_plain(&data) {
|
|
||||||
Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::error!("Impersonating user is not supported");
|
|
||||||
return AuthRes::Failed(None, None);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
|
|
||||||
return AuthRes::Failed(None, None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to convert it to UTF-8
|
|
||||||
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
|
|
||||||
(Ok(u), Ok(p)) => (u, p),
|
|
||||||
_ => {
|
|
||||||
tracing::error!("Username or password contain invalid UTF-8 characters");
|
|
||||||
return AuthRes::Failed(None, None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to connect user
|
|
||||||
match login.login(user, password).await {
|
|
||||||
Ok(_) => AuthRes::Success(user.to_string()),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(err=?e, "login failed");
|
|
||||||
AuthRes::Failed(Some(user.to_string()), None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn progress(&mut self, cmd: ClientCommand, login: &ArcLoginProvider) {
|
|
||||||
let new_state = 'state: {
|
|
||||||
match (std::mem::replace(self, State::Error), cmd) {
|
|
||||||
(Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
|
|
||||||
(Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
|
|
||||||
if version.major != SERVER_MAJOR {
|
|
||||||
tracing::error!(
|
|
||||||
client_major = version.major,
|
|
||||||
server_major = SERVER_MAJOR,
|
|
||||||
"Unsupported client major version"
|
|
||||||
);
|
|
||||||
break 'state Self::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::HandshakeDone
|
|
||||||
}
|
|
||||||
(
|
|
||||||
Self::HandshakeDone { .. },
|
|
||||||
ClientCommand::Auth {
|
|
||||||
id, mech, options, ..
|
|
||||||
},
|
|
||||||
)
|
|
||||||
| (
|
|
||||||
Self::AuthDone { .. },
|
|
||||||
ClientCommand::Auth {
|
|
||||||
id, mech, options, ..
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if mech != Mechanism::Plain {
|
|
||||||
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
|
|
||||||
break 'state Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: AuthRes::Failed(None, None),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
match options.last() {
|
|
||||||
Some(AuthOption::Resp(data)) => Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: self.try_auth_plain(&data, login).await,
|
|
||||||
},
|
|
||||||
_ => Self::AuthPlainProgress { id },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
|
|
||||||
// Check that ID matches
|
|
||||||
if cid != id {
|
|
||||||
tracing::error!(
|
|
||||||
auth_id = id,
|
|
||||||
cont_id = cid,
|
|
||||||
"CONT id does not match AUTH id"
|
|
||||||
);
|
|
||||||
break 'state Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: AuthRes::Failed(None, None),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: self.try_auth_plain(&data, login).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
tracing::error!("This command is not valid in this context");
|
|
||||||
Self::Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
tracing::debug!(state=?new_state, "Made progress");
|
|
||||||
*self = new_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn response(&self) -> Vec<ServerCommand> {
|
|
||||||
let mut srv_cmd: Vec<ServerCommand> = Vec::new();
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::HandshakeDone { .. } => {
|
|
||||||
srv_cmd.push(ServerCommand::Version(Version {
|
|
||||||
major: SERVER_MAJOR,
|
|
||||||
minor: SERVER_MINOR,
|
|
||||||
}));
|
|
||||||
|
|
||||||
srv_cmd.push(ServerCommand::Mech {
|
|
||||||
kind: Mechanism::Plain,
|
|
||||||
parameters: vec![MechanismParameters::PlainText],
|
|
||||||
});
|
|
||||||
|
|
||||||
srv_cmd.push(ServerCommand::Spid(15u64));
|
|
||||||
srv_cmd.push(ServerCommand::Cuid(19350u64));
|
|
||||||
|
|
||||||
let mut cookie = [0u8; 16];
|
|
||||||
thread_rng().fill(&mut cookie);
|
|
||||||
srv_cmd.push(ServerCommand::Cookie(cookie));
|
|
||||||
|
|
||||||
srv_cmd.push(ServerCommand::Done);
|
|
||||||
}
|
|
||||||
Self::AuthPlainProgress { id } => {
|
|
||||||
srv_cmd.push(ServerCommand::Cont {
|
|
||||||
id: *id,
|
|
||||||
data: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: AuthRes::Success(user),
|
|
||||||
} => {
|
|
||||||
srv_cmd.push(ServerCommand::Ok {
|
|
||||||
id: *id,
|
|
||||||
user_id: Some(user.to_string()),
|
|
||||||
extra_parameters: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Self::AuthDone {
|
|
||||||
id,
|
|
||||||
res: AuthRes::Failed(maybe_user, maybe_failcode),
|
|
||||||
} => {
|
|
||||||
srv_cmd.push(ServerCommand::Fail {
|
|
||||||
id: *id,
|
|
||||||
user_id: maybe_user.clone(),
|
|
||||||
code: maybe_failcode.clone(),
|
|
||||||
extra_parameters: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
srv_cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// DOVECOT AUTH TYPES
|
|
||||||
//
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
enum Mechanism {
|
|
||||||
Plain,
|
|
||||||
Login,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum AuthOption {
|
|
||||||
/// Unique session ID. Mainly used for logging.
|
|
||||||
Session(u64),
|
|
||||||
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
|
|
||||||
LocalIp(String),
|
|
||||||
/// Remote client IP
|
|
||||||
RemoteIp(String),
|
|
||||||
/// Local port connected to by the client.
|
|
||||||
LocalPort(u16),
|
|
||||||
/// Remote client port
|
|
||||||
RemotePort(u16),
|
|
||||||
/// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to.
|
|
||||||
RealRemoteIp(String),
|
|
||||||
RealLocalIp(String),
|
|
||||||
RealLocalPort(u16),
|
|
||||||
RealRemotePort(u16),
|
|
||||||
/// TLS SNI name
|
|
||||||
LocalName(String),
|
|
||||||
/// Enable debugging for this lookup.
|
|
||||||
Debug,
|
|
||||||
/// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
|
|
||||||
/// Note: we do not unescape the tabulation, and thus we don't parse the data
|
|
||||||
ForwardViews(Vec<u8>),
|
|
||||||
/// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
|
|
||||||
Secured(Option<String>),
|
|
||||||
/// The value can be “insecure”, “trusted” or “TLS”.
|
|
||||||
Transport(String),
|
|
||||||
/// TLS cipher being used.
|
|
||||||
TlsCipher(String),
|
|
||||||
/// The number of bits in the TLS cipher.
|
|
||||||
/// @FIXME: I don't know how if it's a string or an integer
|
|
||||||
TlsCipherBits(String),
|
|
||||||
/// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
|
|
||||||
TlsPfs(String),
|
|
||||||
/// TLS protocol name (e.g. SSLv3, TLSv1.2)
|
|
||||||
TlsProtocol(String),
|
|
||||||
/// Remote user has presented a valid SSL certificate.
|
|
||||||
ValidClientCert(String),
|
|
||||||
/// Ignore auth penalty tracking for this request
|
|
||||||
NoPenalty,
|
|
||||||
/// Unknown option sent by Postfix
|
|
||||||
NoLogin,
|
|
||||||
/// Username taken from client’s SSL certificate.
|
|
||||||
CertUsername,
|
|
||||||
/// IMAP ID string
|
|
||||||
ClientId,
|
|
||||||
/// An unknown key
|
|
||||||
UnknownPair(String, Vec<u8>),
|
|
||||||
UnknownBool(Vec<u8>),
|
|
||||||
/// Initial response for authentication mechanism.
|
|
||||||
/// NOTE: This must be the last parameter. Everything after it is ignored.
|
|
||||||
/// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
|
|
||||||
/// @FIXME: I don't understand this parameter
|
|
||||||
Resp(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Version {
|
|
||||||
major: u64,
|
|
||||||
minor: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
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.
|
|
||||||
Version(Version),
|
|
||||||
/// CPID finishes the handshake from client.
|
|
||||||
Cpid(u64),
|
|
||||||
Auth {
|
|
||||||
/// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one.
|
|
||||||
id: u64,
|
|
||||||
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
|
|
||||||
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
|
|
||||||
mech: Mechanism,
|
|
||||||
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
|
|
||||||
service: String,
|
|
||||||
/// All the optional parameters
|
|
||||||
options: Vec<AuthOption>,
|
|
||||||
},
|
|
||||||
Cont {
|
|
||||||
/// The <id> must match the <id> of the AUTH command.
|
|
||||||
id: u64,
|
|
||||||
/// Data that will be serialized to / deserialized from base64
|
|
||||||
data: Vec<u8>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum MechanismParameters {
|
|
||||||
/// Anonymous authentication
|
|
||||||
Anonymous,
|
|
||||||
/// Transfers plaintext passwords
|
|
||||||
PlainText,
|
|
||||||
/// Subject to passive (dictionary) attack
|
|
||||||
Dictionary,
|
|
||||||
/// Subject to active (non-dictionary) attack
|
|
||||||
Active,
|
|
||||||
/// Provides forward secrecy between sessions
|
|
||||||
ForwardSecrecy,
|
|
||||||
/// Provides mutual authentication
|
|
||||||
MutualAuth,
|
|
||||||
/// Don’t advertise this as available SASL mechanism (eg. APOP)
|
|
||||||
Private,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum FailCode {
|
|
||||||
/// This is a temporary internal failure, e.g. connection was lost to SQL database.
|
|
||||||
TempFail,
|
|
||||||
/// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok).
|
|
||||||
AuthzFail,
|
|
||||||
/// User is disabled (password may or may not have been correct)
|
|
||||||
UserDisabled,
|
|
||||||
/// User’s password has expired.
|
|
||||||
PassExpired,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
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.
|
|
||||||
Version(Version),
|
|
||||||
/// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
|
|
||||||
/// SPID can be used by authentication client to tell master which server process handled the authentication.
|
|
||||||
Spid(u64),
|
|
||||||
/// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server.
|
|
||||||
/// CUID is currently useful only for APOP authentication.
|
|
||||||
Cuid(u64),
|
|
||||||
Mech {
|
|
||||||
kind: Mechanism,
|
|
||||||
parameters: Vec<MechanismParameters>,
|
|
||||||
},
|
|
||||||
/// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
|
|
||||||
Cookie([u8; 16]),
|
|
||||||
/// DONE finishes the handshake from server.
|
|
||||||
Done,
|
|
||||||
|
|
||||||
Fail {
|
|
||||||
id: u64,
|
|
||||||
user_id: Option<String>,
|
|
||||||
code: Option<FailCode>,
|
|
||||||
extra_parameters: Vec<Vec<u8>>,
|
|
||||||
},
|
|
||||||
Cont {
|
|
||||||
id: u64,
|
|
||||||
data: Option<Vec<u8>>,
|
|
||||||
},
|
|
||||||
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
|
|
||||||
/// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
|
|
||||||
Ok {
|
|
||||||
id: u64,
|
|
||||||
user_id: Option<String>,
|
|
||||||
extra_parameters: Vec<Vec<u8>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// DOVECOT AUTH DECODING
|
|
||||||
//
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use nom::{
|
|
||||||
branch::alt,
|
|
||||||
bytes::complete::{is_not, tag, tag_no_case, take, take_while, take_while1},
|
|
||||||
character::complete::{tab, u16, u64},
|
|
||||||
combinator::{map, opt, recognize, rest, value},
|
|
||||||
error::{Error, ErrorKind},
|
|
||||||
multi::{many1, separated_list0},
|
|
||||||
sequence::{pair, preceded, tuple},
|
|
||||||
IResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
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(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 parameter_str(input: &[u8]) -> IResult<&[u8], String> {
|
|
||||||
let (input, buf) = parameter(input)?;
|
|
||||||
|
|
||||||
std::str::from_utf8(buf)
|
|
||||||
.map(|v| (input, v.to_string()))
|
|
||||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_param_name_char(c: u8) -> bool {
|
|
||||||
is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
|
|
||||||
let (input, buf) = take_while1(is_param_name_char)(input)?;
|
|
||||||
|
|
||||||
std::str::from_utf8(buf)
|
|
||||||
.map(|v| (input, v.to_string()))
|
|
||||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
|
|
||||||
preceded(tag_no_case("service="), parameter_str)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
|
|
||||||
use AuthOption::*;
|
|
||||||
alt((
|
|
||||||
alt((
|
|
||||||
value(Debug, tag_no_case(b"debug")),
|
|
||||||
value(NoPenalty, tag_no_case(b"no-penalty")),
|
|
||||||
value(ClientId, tag_no_case(b"client_id")),
|
|
||||||
value(NoLogin, tag_no_case(b"nologin")),
|
|
||||||
map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
|
|
||||||
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
|
|
||||||
LocalIp(ip)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
|
|
||||||
RemoteIp(ip)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"lport="), u16), |port| {
|
|
||||||
LocalPort(port)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"rport="), u16), |port| {
|
|
||||||
RemotePort(port)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
|
|
||||||
RealRemoteIp(ip)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
|
|
||||||
RealLocalIp(ip)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"real_lport="), u16), |port| {
|
|
||||||
RealLocalPort(port)
|
|
||||||
}),
|
|
||||||
map(preceded(tag_no_case(b"real_rport="), u16), |port| {
|
|
||||||
RealRemotePort(port)
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
alt((
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"local_name="), parameter_str),
|
|
||||||
|name| LocalName(name),
|
|
||||||
),
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"forward_views="), parameter),
|
|
||||||
|views| ForwardViews(views.into()),
|
|
||||||
),
|
|
||||||
map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
|
|
||||||
Secured(Some(info))
|
|
||||||
}),
|
|
||||||
value(Secured(None), tag_no_case(b"secured")),
|
|
||||||
value(CertUsername, tag_no_case(b"cert_username")),
|
|
||||||
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
|
|
||||||
Transport(ts)
|
|
||||||
}),
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"tls_cipher="), parameter_str),
|
|
||||||
|cipher| TlsCipher(cipher),
|
|
||||||
),
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
|
|
||||||
|bits| TlsCipherBits(bits),
|
|
||||||
),
|
|
||||||
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
|
|
||||||
TlsPfs(pfs)
|
|
||||||
}),
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"tls_protocol="), parameter_str),
|
|
||||||
|proto| TlsProtocol(proto),
|
|
||||||
),
|
|
||||||
map(
|
|
||||||
preceded(tag_no_case(b"valid-client-cert="), parameter_str),
|
|
||||||
|cert| ValidClientCert(cert),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
alt((
|
|
||||||
map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
|
|
||||||
map(
|
|
||||||
tuple((parameter_name, tag(b"="), parameter)),
|
|
||||||
|(n, _, v)| UnknownPair(n, v.into()),
|
|
||||||
),
|
|
||||||
map(parameter, |v| UnknownBool(v.into())),
|
|
||||||
)),
|
|
||||||
))(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 // =
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
|
||||||
let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
|
|
||||||
|
|
||||||
let data = base64::engine::general_purpose::STANDARD_NO_PAD
|
|
||||||
.decode(b64)
|
|
||||||
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
|
|
||||||
|
|
||||||
Ok((input, data))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @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, base64));
|
|
||||||
|
|
||||||
let (input, (_, _, id, _, data)) = parser(input)?;
|
|
||||||
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!();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// SASL DECODING
|
|
||||||
//
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
fn not_null(c: u8) -> bool {
|
|
||||||
c != 0x0
|
|
||||||
}
|
|
||||||
|
|
||||||
// impersonated user, login, password
|
|
||||||
fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
|
|
||||||
map(
|
|
||||||
tuple((
|
|
||||||
take_while(not_null),
|
|
||||||
take(1usize),
|
|
||||||
take_while(not_null),
|
|
||||||
take(1usize),
|
|
||||||
rest,
|
|
||||||
)),
|
|
||||||
|(imp, _, user, _, pass)| (imp, user, pass),
|
|
||||||
)(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//
|
|
||||||
// DOVECOT AUTH ENCODING
|
|
||||||
//
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
use tokio_util::bytes::{BufMut, BytesMut};
|
|
||||||
trait Encode {
|
|
||||||
fn encode(&self, out: &mut BytesMut) -> Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tab_enc(out: &mut BytesMut) {
|
|
||||||
out.put(&[0x09][..])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lf_enc(out: &mut BytesMut) {
|
|
||||||
out.put(&[0x0A][..])
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encode for Mechanism {
|
|
||||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Plain => out.put(&b"PLAIN"[..]),
|
|
||||||
Self::Login => out.put(&b"LOGIN"[..]),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encode for MechanismParameters {
|
|
||||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Anonymous => out.put(&b"anonymous"[..]),
|
|
||||||
Self::PlainText => out.put(&b"plaintext"[..]),
|
|
||||||
Self::Dictionary => out.put(&b"dictionary"[..]),
|
|
||||||
Self::Active => out.put(&b"active"[..]),
|
|
||||||
Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
|
|
||||||
Self::MutualAuth => out.put(&b"mutual-auth"[..]),
|
|
||||||
Self::Private => out.put(&b"private"[..]),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encode for FailCode {
|
|
||||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::TempFail => out.put(&b"temp_fail"[..]),
|
|
||||||
Self::AuthzFail => out.put(&b"authz_fail"[..]),
|
|
||||||
Self::UserDisabled => out.put(&b"user_disabled"[..]),
|
|
||||||
Self::PassExpired => out.put(&b"pass_expired"[..]),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encode for ServerCommand {
|
|
||||||
fn encode(&self, out: &mut BytesMut) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Version(Version { major, minor }) => {
|
|
||||||
out.put(&b"VERSION"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(major.to_string().as_bytes());
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(minor.to_string().as_bytes());
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Spid(pid) => {
|
|
||||||
out.put(&b"SPID"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(pid.to_string().as_bytes());
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Cuid(pid) => {
|
|
||||||
out.put(&b"CUID"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(pid.to_string().as_bytes());
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Cookie(cval) => {
|
|
||||||
out.put(&b"COOKIE"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(hex::encode(cval).as_bytes());
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Mech { kind, parameters } => {
|
|
||||||
out.put(&b"MECH"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
kind.encode(out)?;
|
|
||||||
for p in parameters.iter() {
|
|
||||||
tab_enc(out);
|
|
||||||
p.encode(out)?;
|
|
||||||
}
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Done => {
|
|
||||||
out.put(&b"DONE"[..]);
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Cont { id, data } => {
|
|
||||||
out.put(&b"CONT"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(id.to_string().as_bytes());
|
|
||||||
tab_enc(out);
|
|
||||||
if let Some(rdata) = data {
|
|
||||||
let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
|
|
||||||
out.put(b64.as_bytes());
|
|
||||||
}
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Ok {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
extra_parameters,
|
|
||||||
} => {
|
|
||||||
out.put(&b"OK"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(id.to_string().as_bytes());
|
|
||||||
if let Some(user) = user_id {
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(&b"user="[..]);
|
|
||||||
out.put(user.as_bytes());
|
|
||||||
}
|
|
||||||
for p in extra_parameters.iter() {
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(&p[..]);
|
|
||||||
}
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
Self::Fail {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
code,
|
|
||||||
extra_parameters,
|
|
||||||
} => {
|
|
||||||
out.put(&b"FAIL"[..]);
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(id.to_string().as_bytes());
|
|
||||||
if let Some(user) = user_id {
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(&b"user="[..]);
|
|
||||||
out.put(user.as_bytes());
|
|
||||||
}
|
|
||||||
if let Some(code_val) = code {
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(&b"code="[..]);
|
|
||||||
code_val.encode(out)?;
|
|
||||||
}
|
|
||||||
for p in extra_parameters.iter() {
|
|
||||||
tab_enc(out);
|
|
||||||
out.put(&p[..]);
|
|
||||||
}
|
|
||||||
lf_enc(out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue