gateryx 0.1.6

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

use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use url::Url;
use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::{
    ConfigCheckIssue, Error, Result,
    util::{AllowRemoteAny, GDuration, default_true},
};

#[derive(Deserialize, Copy, Clone, Default)]
pub enum AppClientKind {
    #[serde(rename = "http1.0")]
    Http0,
    #[default]
    #[serde(rename = "http1.1", alias = "http1")]
    Http1,
    #[serde(rename = "http2")]
    Http2,
}

impl AppClientKind {
    pub fn insert_gateryx_headers(self) -> bool {
        match self {
            AppClientKind::Http0 => false,
            AppClientKind::Http1 | AppClientKind::Http2 => true,
        }
    }
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Deserialize, Clone, Zeroize, ZeroizeOnDrop)]
#[serde(deny_unknown_fields)]
pub struct Config {
    #[serde(default)]
    pub name: String,
    #[serde(default = "default_true")]
    pub allow_tokens: bool,
    #[serde(default)]
    pub allow_groups: Vec<String>,
    #[serde(default)]
    pub url: String,
    #[zeroize(skip)]
    pub icon: Option<PathBuf>,
    #[serde(default)]
    pub icon_image: Option<Vec<u8>>,
    #[serde(default)]
    pub hosts: Vec<String>,
    pub remote: String,
    #[serde(default)]
    pub compress: bool,
    #[serde(default)]
    #[zeroize(skip)]
    pub client: AppClientKind,
    #[serde(default = "crate::util::default_timeout")]
    #[zeroize(skip)]
    pub timeout: GDuration,
    #[serde(default = "crate::util::default_true")]
    pub use_auth: bool,
    #[serde(default)]
    pub gateryx_api: bool,
    #[serde(default)]
    pub hidden: bool,
    #[serde(default)]
    pub skip_remote_tls_verify: bool,
    #[zeroize(skip)]
    pub websocket: Option<crate::ws::Config>,
    #[zeroize(skip)]
    pub settings: Option<serde_json::Value>,
    #[zeroize(skip)]
    #[serde(default)]
    pub allow: AllowRemoteAny,
}

impl Config {
    pub fn check(&self, config_dir: &Path, app_config_path: &Path) -> Vec<ConfigCheckIssue> {
        let mut issues = Vec::new();
        if self.url.starts_with("gateryx://") {
            // TODO: check virtual apps for issues
            return issues;
        }
        if !self.url.is_empty() && Url::parse(&self.url).is_err() {
            issues.push(ConfigCheckIssue::Warning(format!(
                "{} invalid app url: {}",
                app_config_path.display(),
                self.url
            )));
        }
        if Url::parse(&self.remote).is_err() {
            issues.push(ConfigCheckIssue::Error(format!(
                "{} invalid app remote url: {}",
                app_config_path.display(),
                self.remote
            )));
        }
        if let Some(ref icon) = self.icon {
            let icon_path = if icon.is_absolute() {
                icon.clone()
            } else {
                config_dir.join(icon)
            };
            if !icon_path.exists() {
                issues.push(ConfigCheckIssue::Warning(format!(
                    "{} app icon path does not exist: {}",
                    app_config_path.display(),
                    icon_path.display()
                )));
            }
        }
        if self.hosts.is_empty() {
            issues.push(ConfigCheckIssue::Error(format!(
                "{} no hosts configured for app",
                app_config_path.display()
            )));
        }
        issues
    }
}

#[derive(Default)]
pub struct AppHostMap {
    inner: Mutex<AppHostMapInner>,
}

impl AppHostMap {
    pub async fn has_skip_remote_tls_verify(&self) -> bool {
        self.inner
            .lock()
            .await
            .apps
            .values()
            .any(|config| config.skip_remote_tls_verify)
    }
    pub async fn app_icon(&self, name: &str) -> Option<Vec<u8>> {
        let inner = self.inner.lock().await;
        inner.app_icon(name)
    }
    pub async fn apps(&self) -> Vec<AppView> {
        let inner = self.inner.lock().await;
        inner.apps()
    }
    pub fn apps_admin_view(&self) -> Vec<AdminAppView> {
        let inner = self.inner.blocking_lock();
        inner.apps_admin_view()
    }
    pub fn set_default_app(&self, name: Option<&str>) {
        let mut inner = self.inner.blocking_lock();
        inner.set_default_app(name);
    }
    pub fn add_sync(&self, name: &str, config: Config) -> Result<()> {
        let mut inner = self.inner.blocking_lock();
        inner.add(name, config)
    }
    pub async fn add(&self, name: &str, config: Config) -> Result<()> {
        let mut inner = self.inner.lock().await;
        inner.add(name, config)
    }
    pub async fn remove(&self, name: &str) {
        let mut inner = self.inner.lock().await;
        inner.remove(name);
    }
    pub async fn get_by_host(&self, host: &str) -> Option<Arc<Config>> {
        let inner = self.inner.lock().await;
        inner
            .app_hosts
            .get(host)
            .cloned()
            .or_else(|| inner.default.as_ref().map(|(_, c)| c.clone()))
    }
    pub async fn get_by_name(&self, id: &str) -> Option<Arc<Config>> {
        let inner = self.inner.lock().await;
        inner.apps.get(id).cloned()
    }
    pub fn default_app_present(&self) -> bool {
        let inner = self.inner.blocking_lock();
        inner.default_app_present()
    }
}

#[derive(Default)]
struct AppHostMapInner {
    default_app_name: Option<String>,
    apps: BTreeMap<String, Arc<Config>>,
    app_hosts: BTreeMap<String, Arc<Config>>,
    default: Option<(String, Arc<Config>)>,
}

#[derive(Serialize, Deserialize)]
pub struct AppView {
    pub name: String,
    pub display_name: String,
    pub has_icon: bool,
    pub allow_tokens: bool,
    pub url: String,
    #[serde(skip)]
    pub allow_groups: Vec<String>,
    pub hidden: bool,
}

#[derive(Serialize, Deserialize)]
pub struct AdminAppView {
    pub name: String,
    pub display_name: String,
    pub has_icon: bool,
    pub allow_tokens: bool,
    pub url: String,
    pub hosts: Vec<String>,
    pub allow_groups: Vec<String>,
    pub hidden: bool,
}

impl AppHostMapInner {
    fn app_icon(&self, name: &str) -> Option<Vec<u8>> {
        self.apps
            .get(name)
            .and_then(|config| config.icon_image.clone())
    }
    fn apps(&self) -> Vec<AppView> {
        let mut res: Vec<AppView> = self
            .apps
            .iter()
            .filter_map(|(n, config)| {
                if config.hidden {
                    None
                } else {
                    Some(AppView {
                        name: n.clone(),
                        display_name: config.name.clone(),
                        has_icon: config.icon.is_some(),
                        allow_tokens: config.allow_tokens,
                        url: config.url.clone(),
                        allow_groups: config.allow_groups.clone(),
                        hidden: config.hidden,
                    })
                }
            })
            .collect();
        res.sort_by(|a, b| a.display_name.cmp(&b.display_name));
        res
    }
    fn apps_admin_view(&self) -> Vec<AdminAppView> {
        let mut res: Vec<AdminAppView> = self
            .apps
            .iter()
            .map(|(n, config)| AdminAppView {
                name: n.clone(),
                display_name: config.name.clone(),
                has_icon: config.icon.is_some(),
                allow_tokens: config.allow_tokens,
                url: config.url.clone(),
                hosts: config.hosts.clone(),
                allow_groups: config.allow_groups.clone(),
                hidden: config.hidden,
            })
            .collect();
        res.sort_by(|a, b| a.display_name.cmp(&b.display_name));
        res
    }
    fn set_default_app(&mut self, name: Option<&str>) {
        let Some(name) = name else {
            self.default_app_name = None;
            self.default = None;
            return;
        };
        if let Some(config) = self.apps.get(name) {
            self.default = Some((name.to_string(), config.clone()));
        }
        self.default_app_name = Some(name.to_string());
    }
    fn add(&mut self, name: &str, config: Config) -> Result<()> {
        let config = Arc::new(config);
        if self.apps.contains_key(name) {
            return Err(Error::AppAlreadyExists);
        }
        for h in &config.hosts {
            if self.app_hosts.contains_key(h) {
                return Err(Error::HostAlreadyExists);
            }
            self.app_hosts.insert(h.clone(), config.clone());
        }
        if self.default_app_name.as_ref().is_some_and(|n| n == name) {
            self.default = Some((name.to_string(), config.clone()));
        }
        self.apps.insert(name.to_string(), config);
        Ok(())
    }
    fn remove(&mut self, name: &str) {
        if let Some(config) = self.apps.remove(name) {
            for h in &config.hosts {
                self.app_hosts.remove(h);
            }
        }
        if self.default.as_ref().is_some_and(|(n, _)| n == name) {
            self.default = None;
        }
    }
    fn default_app_present(&self) -> bool {
        self.default.is_some()
    }
}