Directory implementation - part 3
This commit is contained in:
@@ -7,7 +7,9 @@ resolver = "2"
|
||||
[dependencies]
|
||||
utils = { path = "../utils" }
|
||||
smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" }
|
||||
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
|
||||
mail-send = { git = "https://github.com/stalwartlabs/mail-send", default-features = false, features = ["cram-md5", "skip-ehlo"] }
|
||||
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
|
||||
tokio = { version = "1.23", features = ["net"] }
|
||||
tokio-rustls = { version = "0.24.0"}
|
||||
rustls = "0.21.0"
|
||||
@@ -18,6 +20,14 @@ async-trait = "0.1.68"
|
||||
ahash = { version = "0.8" }
|
||||
tracing = "0.1"
|
||||
lru-cache = "0.1.2"
|
||||
pwhash = "1"
|
||||
password-hash = "0.5.0"
|
||||
argon2 = "0.5.0"
|
||||
pbkdf2 = {version = "0.12.1", features = ["simple"] }
|
||||
scrypt = "0.11.0"
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.6"
|
||||
md5 = "0.7.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.23", features = ["full"] }
|
||||
|
||||
@@ -1,37 +1,89 @@
|
||||
use bb8::{ManageConnection, Pool};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use utils::config::Config;
|
||||
|
||||
use ahash::AHashMap;
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
|
||||
use crate::{
|
||||
imap::ImapDirectory, ldap::LdapDirectory, smtp::SmtpDirectory, sql::SqlDirectory, Directory,
|
||||
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
|
||||
sql::SqlDirectory, DirectoryConfig, Lookup,
|
||||
};
|
||||
|
||||
pub trait ConfigDirectory {
|
||||
fn parse_directory(&self) -> utils::config::Result<AHashMap<String, Arc<dyn Directory>>>;
|
||||
fn parse_directory(&self) -> utils::config::Result<DirectoryConfig>;
|
||||
}
|
||||
|
||||
impl ConfigDirectory for Config {
|
||||
fn parse_directory(&self) -> utils::config::Result<AHashMap<String, Arc<dyn Directory>>> {
|
||||
let mut directories = AHashMap::new();
|
||||
fn parse_directory(&self) -> utils::config::Result<DirectoryConfig> {
|
||||
let mut config = DirectoryConfig {
|
||||
directories: AHashMap::new(),
|
||||
lookups: AHashMap::new(),
|
||||
};
|
||||
for id in self.sub_keys("directory") {
|
||||
directories.insert(
|
||||
id.to_string(),
|
||||
match self.value_require(("directory", id, "protocol"))? {
|
||||
"ldap" => LdapDirectory::from_config(self, ("directory", id))?,
|
||||
"sql" => SqlDirectory::from_config(self, ("directory", id))?,
|
||||
"imap" => ImapDirectory::from_config(self, ("directory", id))?,
|
||||
"smtp" => SmtpDirectory::from_config(self, ("directory", id), false)?,
|
||||
"lmtp" => SmtpDirectory::from_config(self, ("directory", id), true)?,
|
||||
unknown => {
|
||||
return Err(format!("Unknown directory type: {unknown:?}"));
|
||||
// Parse directory
|
||||
let protocol = self.value_require(("directory", id, "protocol"))?;
|
||||
let directory = match protocol {
|
||||
"ldap" => LdapDirectory::from_config(self, ("directory", id))?,
|
||||
"sql" => SqlDirectory::from_config(self, ("directory", id))?,
|
||||
"imap" => ImapDirectory::from_config(self, ("directory", id))?,
|
||||
"smtp" => SmtpDirectory::from_config(self, ("directory", id), false)?,
|
||||
"lmtp" => SmtpDirectory::from_config(self, ("directory", id), true)?,
|
||||
"memory" => MemoryDirectory::from_config(self, ("directory", id))?,
|
||||
unknown => {
|
||||
return Err(format!("Unknown directory type: {unknown:?}"));
|
||||
}
|
||||
};
|
||||
|
||||
// Parse lookups
|
||||
let is_remote = protocol != "memory";
|
||||
for lookup_id in self.sub_keys(("directory", id, "lookup")) {
|
||||
let lookup = if is_remote {
|
||||
Lookup::Directory {
|
||||
directory: directory.clone(),
|
||||
query: self
|
||||
.value_require(("directory", id, "lookup", lookup_id))?
|
||||
.to_string(),
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let mut list = AHashSet::new();
|
||||
for (_, value) in self.values(("directory", id, "lookup", lookup_id)) {
|
||||
if let Some(path) = value.strip_prefix("file://") {
|
||||
for line in BufReader::new(File::open(path).map_err(|err| {
|
||||
format!(
|
||||
"Failed to read file {path:?} for list {id}/{lookup_id}: {err}"
|
||||
)
|
||||
})?)
|
||||
.lines()
|
||||
{
|
||||
let line_ = line.map_err(|err| {
|
||||
format!("Failed to read file {path:?} for list {id}/{lookup_id}: {err}")
|
||||
})?;
|
||||
let line = line_.trim();
|
||||
if !line.is_empty() {
|
||||
list.insert(line.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
list.insert(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Lookup::List { list }
|
||||
};
|
||||
config
|
||||
.lookups
|
||||
.insert(format!("{id}/{lookup_id}"), Arc::new(lookup));
|
||||
}
|
||||
|
||||
config.directories.insert(id.to_string(), directory);
|
||||
}
|
||||
|
||||
Ok(directories)
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ impl LdapDirectory {
|
||||
);
|
||||
|
||||
let mut mappings = LdapMappings {
|
||||
base_dn: config.value_require((&prefix, "address"))?.to_string(),
|
||||
base_dn: config.value_require((&prefix, "base-dn"))?.to_string(),
|
||||
filter_login: LdapFilter::from_config(config, (&prefix, "filter.login"))?,
|
||||
filter_name: LdapFilter::from_config(config, (&prefix, "filter.name"))?,
|
||||
filter_email: LdapFilter::from_config(config, (&prefix, "filter.email"))?,
|
||||
@@ -73,7 +73,7 @@ impl LdapDirectory {
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
attr_quota: config
|
||||
.values((&prefix, "attributes."))
|
||||
.values((&prefix, "attributes.quota"))
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
attrs_principal: vec!["objectClass".to_string()],
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Directory for LdapDirectory {
|
||||
.await
|
||||
{
|
||||
Ok(Some(principal)) => {
|
||||
if principal.secret.as_ref().map_or(false, |s| s == secret) {
|
||||
if principal.verify_secret(secret) {
|
||||
Ok(Some(principal))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -49,7 +49,7 @@ impl Directory for LdapDirectory {
|
||||
let mut ids = Vec::with_capacity(principal.member_of.len());
|
||||
for group in &principal.member_of {
|
||||
let (rs, _res) = if group.contains('=') {
|
||||
conn.search(group, Scope::Base, "", &self.mappings.attr_id)
|
||||
conn.search(group, Scope::Base, "objectClass=*", &self.mappings.attr_id)
|
||||
.await?
|
||||
.success()?
|
||||
} else {
|
||||
@@ -126,11 +126,12 @@ impl Directory for LdapDirectory {
|
||||
let mut ids = Vec::new();
|
||||
for entry in rs {
|
||||
let entry = SearchEntry::construct(entry);
|
||||
for attr in &self.mappings.attr_id {
|
||||
'outer: for attr in &self.mappings.attr_id {
|
||||
if let Some(values) = entry.attrs.get(attr) {
|
||||
for id in values {
|
||||
if let Ok(id) = id.parse() {
|
||||
ids.push(id);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,16 +280,20 @@ impl LdapMappings {
|
||||
..Default::default()
|
||||
};
|
||||
for (attr, value) in entry.attrs {
|
||||
if self.attr_id.contains(&attr) {
|
||||
if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() {
|
||||
principal.id = id;
|
||||
if let Some(idx) = self.attr_id.iter().position(|a| a == &attr) {
|
||||
if principal.id == u32::MAX || idx == 0 {
|
||||
if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() {
|
||||
principal.id = id;
|
||||
}
|
||||
}
|
||||
} else if self.attr_name.contains(&attr) {
|
||||
principal.name = value.into_iter().next().unwrap_or_default();
|
||||
} else if self.attr_secret.contains(&attr) {
|
||||
principal.secret = value.into_iter().next();
|
||||
} else if self.attr_description.contains(&attr) {
|
||||
principal.description = value.into_iter().next();
|
||||
principal.secrets.extend(value);
|
||||
} else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) {
|
||||
if principal.description.is_none() || idx == 0 {
|
||||
principal.description = value.into_iter().next();
|
||||
}
|
||||
} else if self.attr_groups.contains(&attr) {
|
||||
principal.member_of.extend(value);
|
||||
} else if self.attr_quota.contains(&attr) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use bb8::RunError;
|
||||
use imap::ImapError;
|
||||
use ldap3::LdapError;
|
||||
@@ -6,21 +9,23 @@ use mail_send::Credentials;
|
||||
pub mod config;
|
||||
pub mod imap;
|
||||
pub mod ldap;
|
||||
pub mod memory;
|
||||
pub mod secret;
|
||||
pub mod smtp;
|
||||
pub mod sql;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Principal {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub secret: Option<String>,
|
||||
pub secrets: Vec<String>,
|
||||
pub typ: Type,
|
||||
pub description: Option<String>,
|
||||
pub quota: u32,
|
||||
pub member_of: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Individual,
|
||||
Group,
|
||||
@@ -52,6 +57,72 @@ pub trait Directory: Sync + Send {
|
||||
async fn vrfy(&self, address: &str) -> Result<Vec<String>>;
|
||||
async fn expn(&self, address: &str) -> Result<Vec<String>>;
|
||||
async fn query(&self, query: &str, params: &[&str]) -> Result<bool>;
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Lookup {
|
||||
Directory {
|
||||
directory: Arc<dyn Directory>,
|
||||
query: String,
|
||||
},
|
||||
List {
|
||||
list: AHashSet<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Lookup {
|
||||
pub async fn contains(&self, item: &str) -> Option<bool> {
|
||||
match self {
|
||||
Lookup::Directory { directory, query } => match directory.query(query, &[item]).await {
|
||||
Ok(result) => result.into(),
|
||||
Err(_) => None,
|
||||
},
|
||||
Lookup::List { list } => list.contains(item).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Lookup {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Lookup::Directory { query, .. }, Lookup::Directory { query: other, .. }) => {
|
||||
query == other
|
||||
}
|
||||
(Lookup::List { list }, Lookup::List { list: other }) => list == other,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Lookup {}
|
||||
|
||||
impl Debug for dyn Directory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Directory")
|
||||
.field("type", &self.type_name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Lookup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Directory { query, .. } => {
|
||||
f.debug_struct("Directory").field("query", query).finish()
|
||||
}
|
||||
Self::List { list } => f.debug_struct("List").field("list", list).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DirectoryConfig {
|
||||
pub directories: AHashMap<String, Arc<dyn Directory>>,
|
||||
pub lookups: AHashMap<String, Arc<Lookup>>,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, DirectoryError>;
|
||||
@@ -161,51 +232,3 @@ impl DirectoryError {
|
||||
DirectoryError::TimedOut
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
|
||||
|
||||
use crate::ldap::{Bind, LdapConnectionManager};
|
||||
|
||||
#[tokio::test]
|
||||
async fn ldap() {
|
||||
let manager = LdapConnectionManager::new(
|
||||
"ldap://localhost:3893".to_string(),
|
||||
LdapConnSettings::new(),
|
||||
Bind::new(
|
||||
"cn=serviceuser,ou=svcaccts,dc=example,dc=com".into(),
|
||||
"mysecret".into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
let pool = bb8::Pool::builder()
|
||||
.min_idle(None)
|
||||
.max_size(10)
|
||||
.max_lifetime(std::time::Duration::from_secs(30 * 60).into())
|
||||
.idle_timeout(std::time::Duration::from_secs(10 * 60).into())
|
||||
.connection_timeout(std::time::Duration::from_secs(30))
|
||||
.test_on_check_out(true)
|
||||
.build(manager)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut ldap = pool.get().await.unwrap();
|
||||
|
||||
let (rs, _res) = ldap
|
||||
.search(
|
||||
"dc=example,dc=com",
|
||||
Scope::Subtree,
|
||||
"(&(objectClass=posixAccount)(cn=johndoe))",
|
||||
vec!["cocomiel", "cn", "uidNumber"], //Vec::<String>::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.success()
|
||||
.unwrap();
|
||||
for entry in rs {
|
||||
println!("{:#?}", SearchEntry::construct(entry));
|
||||
}
|
||||
ldap.unbind().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
100
crates/directory/src/memory/config.rs
Normal file
100
crates/directory/src/memory/config.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{Directory, Principal, Type};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
impl MemoryDirectory {
|
||||
pub fn from_config(
|
||||
config: &Config,
|
||||
prefix: impl AsKey,
|
||||
) -> utils::config::Result<Arc<dyn Directory>> {
|
||||
let prefix = prefix.as_key();
|
||||
let mut directory = MemoryDirectory::default();
|
||||
|
||||
for lookup_id in config.sub_keys((prefix.as_str(), "users")) {
|
||||
let id = directory.principals.len() as u32;
|
||||
let name = config
|
||||
.value_require((prefix.as_str(), "users", lookup_id, "name"))?
|
||||
.to_string();
|
||||
directory.names.insert(name.clone(), id);
|
||||
directory.principals.push(Principal {
|
||||
id,
|
||||
name,
|
||||
secrets: config
|
||||
.values((prefix.as_str(), "users", lookup_id, "secret"))
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
typ: Type::Individual,
|
||||
description: config
|
||||
.value((prefix.as_str(), "users", lookup_id, "description"))
|
||||
.map(|v| v.to_string()),
|
||||
quota: config
|
||||
.property((prefix.as_str(), "users", lookup_id, "quota"))?
|
||||
.unwrap_or(0),
|
||||
member_of: config
|
||||
.values((prefix.as_str(), "users", lookup_id, "member-of"))
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
});
|
||||
let mut emails = Vec::new();
|
||||
for (pos, (_, email)) in config
|
||||
.values((prefix.as_str(), "users", lookup_id, "email"))
|
||||
.enumerate()
|
||||
{
|
||||
directory
|
||||
.emails_to_ids
|
||||
.entry(email.to_string())
|
||||
.or_default()
|
||||
.push(if pos > 0 {
|
||||
EmailType::Alias(id)
|
||||
} else {
|
||||
EmailType::Primary(id)
|
||||
});
|
||||
|
||||
emails.push(if pos > 0 {
|
||||
EmailType::Alias(email.to_string())
|
||||
} else {
|
||||
EmailType::Primary(email.to_string())
|
||||
});
|
||||
}
|
||||
for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) {
|
||||
directory
|
||||
.emails_to_ids
|
||||
.entry(email.to_string())
|
||||
.or_default()
|
||||
.push(EmailType::List(id));
|
||||
emails.push(EmailType::List(email.to_string()));
|
||||
}
|
||||
directory.ids_to_email.insert(id, emails);
|
||||
}
|
||||
|
||||
for lookup_id in config.sub_keys((prefix.as_str(), "groups")) {
|
||||
let id = directory.principals.len() as u32;
|
||||
let name = config
|
||||
.value_require((prefix.as_str(), "groups", lookup_id, "name"))?
|
||||
.to_string();
|
||||
directory.names.insert(name.clone(), id);
|
||||
directory.principals.push(Principal {
|
||||
id,
|
||||
name,
|
||||
secrets: vec![],
|
||||
typ: Type::Group,
|
||||
description: config
|
||||
.value((prefix.as_str(), "groups", lookup_id, "description"))
|
||||
.map(|v| v.to_string()),
|
||||
quota: config
|
||||
.property((prefix.as_str(), "groups", lookup_id, "quota"))?
|
||||
.unwrap_or(0),
|
||||
member_of: config
|
||||
.values((prefix.as_str(), "groups", lookup_id, "member-of"))
|
||||
.map(|(_, v)| v.to_string())
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new(directory))
|
||||
}
|
||||
}
|
||||
115
crates/directory/src/memory/lookup.rs
Normal file
115
crates/directory/src/memory/lookup.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use mail_send::Credentials;
|
||||
|
||||
use crate::{Directory, DirectoryError, Principal};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for MemoryDirectory {
|
||||
async fn authenticate(
|
||||
&self,
|
||||
credentials: &Credentials<String>,
|
||||
) -> crate::Result<Option<Principal>> {
|
||||
let (username, secret) = match credentials {
|
||||
Credentials::Plain { username, secret } => (username, secret),
|
||||
Credentials::OAuthBearer { token } => (token, token),
|
||||
Credentials::XOauth2 { username, secret } => (username, secret),
|
||||
};
|
||||
match self
|
||||
.names
|
||||
.get(username)
|
||||
.and_then(|id| self.principals.get(*id as usize))
|
||||
{
|
||||
Some(principal) if principal.verify_secret(secret) => Ok(Some(principal.clone())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn principal_by_name(&self, name: &str) -> crate::Result<Option<Principal>> {
|
||||
Ok(self
|
||||
.names
|
||||
.get(name)
|
||||
.and_then(|id| self.principals.get(*id as usize))
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn principal_by_id(&self, id: u32) -> crate::Result<Option<Principal>> {
|
||||
Ok(self.principals.get(id as usize).cloned())
|
||||
}
|
||||
|
||||
async fn member_of(&self, principal: &Principal) -> crate::Result<Vec<u32>> {
|
||||
let mut result = Vec::with_capacity(principal.member_of.len());
|
||||
for member in &principal.member_of {
|
||||
if let Some(id) = self.names.get(member) {
|
||||
result.push(*id);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn emails_by_id(&self, id: u32) -> crate::Result<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(emails) = self.ids_to_email.get(&id) {
|
||||
for email in emails {
|
||||
match email {
|
||||
EmailType::Primary(email) | EmailType::Alias(email) => {
|
||||
result.push(email.clone())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
Ok(self
|
||||
.emails_to_ids
|
||||
.get(address)
|
||||
.map(|ids| {
|
||||
ids.iter()
|
||||
.map(|t| match t {
|
||||
EmailType::Primary(id) | EmailType::Alias(id) | EmailType::List(id) => *id,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||
Ok(self.emails_to_ids.get(address).is_some())
|
||||
}
|
||||
|
||||
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
for (key, value) in &self.emails_to_ids {
|
||||
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) {
|
||||
result.push(key.clone())
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
for (key, value) in &self.emails_to_ids {
|
||||
if key == address {
|
||||
for item in value {
|
||||
if let EmailType::List(id) = item {
|
||||
for addr in self.ids_to_email.get(id).unwrap() {
|
||||
if let EmailType::Primary(addr) = addr {
|
||||
result.push(addr.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn query(&self, _query: &str, _params: &[&str]) -> crate::Result<bool> {
|
||||
Err(DirectoryError::unsupported("memory", "query"))
|
||||
}
|
||||
}
|
||||
20
crates/directory/src/memory/mod.rs
Normal file
20
crates/directory/src/memory/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use ahash::AHashMap;
|
||||
|
||||
use crate::Principal;
|
||||
|
||||
pub mod config;
|
||||
pub mod lookup;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemoryDirectory {
|
||||
principals: Vec<Principal>,
|
||||
names: AHashMap<String, u32>,
|
||||
emails_to_ids: AHashMap<String, Vec<EmailType<u32>>>,
|
||||
ids_to_email: AHashMap<u32, Vec<EmailType<String>>>,
|
||||
}
|
||||
|
||||
enum EmailType<T> {
|
||||
Primary(T),
|
||||
Alias(T),
|
||||
List(T),
|
||||
}
|
||||
159
crates/directory/src/secret.rs
Normal file
159
crates/directory/src/secret.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use argon2::Argon2;
|
||||
use mail_builder::encoders::base64::base64_encode;
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use password_hash::PasswordHash;
|
||||
use pbkdf2::Pbkdf2;
|
||||
use pwhash::{bcrypt, bsdi_crypt, md5_crypt, sha1_crypt, sha256_crypt, sha512_crypt, unix_crypt};
|
||||
use scrypt::Scrypt;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
use sha2::Sha256;
|
||||
use sha2::Sha512;
|
||||
|
||||
use crate::Principal;
|
||||
|
||||
impl Principal {
|
||||
pub fn verify_secret(&self, secret: &str) -> bool {
|
||||
self.secrets.iter().any(|s| verify_secret_hash(s, secret))
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
|
||||
if hashed_secret.starts_with('$') {
|
||||
if hashed_secret.starts_with("$argon2")
|
||||
|| hashed_secret.starts_with("$pbkdf2")
|
||||
|| hashed_secret.starts_with("$scrypt")
|
||||
{
|
||||
match PasswordHash::new(hashed_secret) {
|
||||
Ok(hash) => hash
|
||||
.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], secret)
|
||||
.is_ok(),
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
context = "directory",
|
||||
event = "error",
|
||||
hash = hashed_secret,
|
||||
"Invalid password hash"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else if hashed_secret.starts_with("$2") {
|
||||
// Blowfish crypt
|
||||
bcrypt::verify(secret, hashed_secret)
|
||||
} else if hashed_secret.starts_with("$6$") {
|
||||
// SHA-512 crypt
|
||||
sha512_crypt::verify(secret, hashed_secret)
|
||||
} else if hashed_secret.starts_with("$5$") {
|
||||
// SHA-256 crypt
|
||||
sha256_crypt::verify(secret, hashed_secret)
|
||||
} else if hashed_secret.starts_with("$sha1") {
|
||||
// SHA-1 crypt
|
||||
sha1_crypt::verify(secret, hashed_secret)
|
||||
} else if hashed_secret.starts_with("$1") {
|
||||
// MD5 based hash
|
||||
md5_crypt::verify(secret, hashed_secret)
|
||||
} else {
|
||||
// Unknown hash
|
||||
tracing::warn!(
|
||||
context = "directory",
|
||||
event = "error",
|
||||
hash = hashed_secret,
|
||||
"Invalid password hash"
|
||||
);
|
||||
false
|
||||
}
|
||||
} else if hashed_secret.starts_with('_') {
|
||||
// Enhanced DES-based hash
|
||||
bsdi_crypt::verify(secret, hashed_secret)
|
||||
} else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') {
|
||||
if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') {
|
||||
match algo {
|
||||
"SHA" => {
|
||||
// SHA-1
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
|
||||
.unwrap()
|
||||
== hashed_secret
|
||||
}
|
||||
"SSHA" => {
|
||||
// Salted SHA-1
|
||||
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
||||
let hash = decoded.get(..20).unwrap_or_default();
|
||||
let salt = decoded.get(20..).unwrap_or_default();
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
hasher.update(salt);
|
||||
&hasher.finalize()[..] == hash
|
||||
}
|
||||
"SHA256" => {
|
||||
// Verify hash
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
|
||||
.unwrap()
|
||||
== hashed_secret
|
||||
}
|
||||
"SSHA256" => {
|
||||
// Salted SHA-256
|
||||
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
||||
let hash = decoded.get(..32).unwrap_or_default();
|
||||
let salt = decoded.get(32..).unwrap_or_default();
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
hasher.update(salt);
|
||||
&hasher.finalize()[..] == hash
|
||||
}
|
||||
"SHA512" => {
|
||||
// SHA-512
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
|
||||
.unwrap()
|
||||
== hashed_secret
|
||||
}
|
||||
"SSHA512" => {
|
||||
// Salted SHA-512
|
||||
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
|
||||
let hash = decoded.get(..64).unwrap_or_default();
|
||||
let salt = decoded.get(64..).unwrap_or_default();
|
||||
let mut hasher = Sha512::new();
|
||||
hasher.update(secret.as_bytes());
|
||||
hasher.update(salt);
|
||||
&hasher.finalize()[..] == hash
|
||||
}
|
||||
"MD5" => {
|
||||
// MD5
|
||||
let digest = md5::compute(secret.as_bytes());
|
||||
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
|
||||
== hashed_secret
|
||||
}
|
||||
"CRYPT" | "crypt" => {
|
||||
// Unix crypt
|
||||
unix_crypt::verify(secret, hashed_secret)
|
||||
}
|
||||
"PLAIN" | "plain" | "CLEAR" | "clear" => hashed_secret == secret,
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
context = "directory",
|
||||
event = "error",
|
||||
algorithm = algo,
|
||||
"Unsupported password hash algorithm"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
context = "directory",
|
||||
event = "error",
|
||||
hash = hashed_secret,
|
||||
"Invalid password hash"
|
||||
);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
hashed_secret == secret
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use sqlx::any::AnyPoolOptions;
|
||||
use sqlx::any::{install_default_drivers, AnyPoolOptions};
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::Directory;
|
||||
@@ -14,7 +14,7 @@ impl SqlDirectory {
|
||||
) -> utils::config::Result<Arc<dyn Directory>> {
|
||||
let prefix = prefix.as_key();
|
||||
let address = config.value_require((&prefix, "address"))?;
|
||||
|
||||
install_default_drivers();
|
||||
let pool = AnyPoolOptions::new()
|
||||
.max_connections(
|
||||
config
|
||||
@@ -64,27 +64,27 @@ impl SqlDirectory {
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_name: config
|
||||
.value((&prefix, "column.name"))
|
||||
.value((&prefix, "columns.name"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_description: config
|
||||
.value((&prefix, "column.description"))
|
||||
.value((&prefix, "columns.description"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_secret: config
|
||||
.value((&prefix, "column.secret"))
|
||||
.value((&prefix, "columns.secret"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_id: config
|
||||
.value((&prefix, "column.id"))
|
||||
.value((&prefix, "columns.id"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_quota: config
|
||||
.value((&prefix, "column.quota"))
|
||||
.value((&prefix, "columns.quota"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
column_type: config
|
||||
.value((&prefix, "column.type"))
|
||||
.value((&prefix, "columns.type"))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Directory for SqlDirectory {
|
||||
.await?
|
||||
{
|
||||
self.mappings.row_to_principal(row).map(|p| {
|
||||
if p.secret.as_ref().map_or(false, |s| s == secret) {
|
||||
if p.verify_secret(secret) {
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
@@ -85,7 +85,7 @@ impl Directory for SqlDirectory {
|
||||
}
|
||||
|
||||
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
|
||||
sqlx::query(&self.mappings.query_recipients)
|
||||
.bind(address)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
@@ -136,7 +136,9 @@ impl SqlMappings {
|
||||
} else if name.eq_ignore_ascii_case(&self.column_name) {
|
||||
principal.name = row.try_get::<Option<String>, _>(idx)?.unwrap_or_default();
|
||||
} else if name.eq_ignore_ascii_case(&self.column_secret) {
|
||||
principal.secret = row.try_get::<Option<String>, _>(idx)?;
|
||||
if let Some(secret) = row.try_get::<Option<String>, _>(idx)? {
|
||||
principal.secrets.push(secret);
|
||||
}
|
||||
} else if name.eq_ignore_ascii_case(&self.column_type) {
|
||||
if let Some(typ) = row.try_get::<Option<String>, _>(idx)? {
|
||||
match typ.as_str() {
|
||||
@@ -148,7 +150,7 @@ impl SqlMappings {
|
||||
} else if name.eq_ignore_ascii_case(&self.column_description) {
|
||||
principal.description = row.try_get::<Option<String>, _>(idx)?;
|
||||
} else if name.eq_ignore_ascii_case(&self.column_quota) {
|
||||
principal.quota = row.try_get::<i64, _>(idx)? as u32;
|
||||
principal.quota = row.try_get::<i64, _>(idx).unwrap_or_default() as u32;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct SqlDirectory {
|
||||
mappings: SqlMappings,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SqlMappings {
|
||||
query_login: String,
|
||||
query_name: String,
|
||||
|
||||
Reference in New Issue
Block a user