gateryx 0.1.6

Secure HTTP gateway for IoT and Industrial applications
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime};

use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tracing::{error, info, trace};

use super::{AuthResult, Authenticator};
use crate::Result;
use crate::authenticator::RandomSleeper;

pub struct HtpasswdAuthenticator {
    inner: Arc<Mutex<HtpasswdAuthenticatorInner>>,
    worker: JoinHandle<()>,
}

impl Drop for HtpasswdAuthenticator {
    fn drop(&mut self) {
        self.worker.abort();
    }
}

struct HtpasswdAuthenticatorInner {
    mtime: SystemTime,
    path: PathBuf,
    passwords: BTreeMap<String, String>,
}

#[async_trait::async_trait]
impl Authenticator for HtpasswdAuthenticator {
    async fn user_groups(&self, _login: &str) -> Result<Vec<String>> {
        Ok(vec![])
    }
    async fn verify(&self, login: &str, password: &str, _otp: Option<&str>) -> AuthResult {
        let random_sleeper = RandomSleeper::new(100..300);
        match self.verify_password(login, password).await {
            Ok(v) => {
                random_sleeper.sleep().await;
                v
            }
            Err(e) => {
                error!(%e, "error verifying password");
                AuthResult::Error
            }
        }
    }
    async fn present(&self, login: &str) -> bool {
        let inner = self.inner.lock().await;
        inner.passwords.contains_key(login)
    }
}

impl HtpasswdAuthenticator {
    pub async fn create<P: AsRef<Path>>(password_file: P) -> Result<Self> {
        info!(path = %password_file.as_ref().display(), "creating htpasswd authenticator");
        let mtime = tokio::fs::metadata(&password_file).await?.modified()?;
        let mut inner = HtpasswdAuthenticatorInner {
            mtime,
            path: password_file.as_ref().to_path_buf(),
            passwords: <_>::default(),
        };
        inner.reload().await?;
        let inner = Arc::new(Mutex::new(inner));
        // the worker is spawned before priv. drop to have full access to the password file
        let worker = tokio::spawn(Self::reload_worker(inner.clone()));
        Ok(Self { inner, worker })
    }
    async fn verify_password(&self, login: &str, password: &str) -> Result<AuthResult> {
        let inner = self.inner.lock().await;
        if let Some(hash) = inner.passwords.get(login) {
            if !bcrypt::verify(password, hash).unwrap_or(false) {
                return Ok(AuthResult::Failure);
            }
            return Ok(AuthResult::Success { groups: vec![] });
        }
        Ok(AuthResult::Failure)
    }
    async fn reload_worker(inner: Arc<Mutex<HtpasswdAuthenticatorInner>>) {
        loop {
            tokio::time::sleep(Duration::from_secs(5)).await;
            if let Err(e) = inner.lock().await.reload_if_required().await {
                error!(%e, "error reloading password file");
                tokio::time::sleep(Duration::from_secs(10)).await;
            }
        }
    }
}

impl HtpasswdAuthenticatorInner {
    async fn reload_if_required(&mut self) -> Result<()> {
        let mtime = tokio::fs::metadata(&self.path).await?.modified()?;
        if mtime > self.mtime {
            self.reload().await?;
            self.mtime = mtime;
        }
        Ok(())
    }
    async fn reload(&mut self) -> Result<()> {
        info!(path = %self.path.display(), "loading password file");
        let data = tokio::fs::read_to_string(&self.path).await?;
        let mut map = BTreeMap::new();
        for line in data.trim().lines() {
            let parts: Vec<&str> = line.trim().split(':').collect();
            if parts.len() != 2 {
                continue;
            }
            if parts[0].starts_with('#') {
                continue;
            }
            let login = parts[0].trim().to_owned();
            trace!(login = login, "loaded password");
            map.insert(login, parts[1].trim().to_owned());
        }
        self.passwords = map;
        Ok(())
    }
}