vane 0.8.15

A flow-based reverse proxy with multi-layer routing and programmable pipelines.
/* src/plugins/core/external.rs */

use crate::common::config::env_loader;
use crate::engine::interfaces::ConnectionObject;
use crate::engine::interfaces::{
	ExternalPluginConfig, ExternalPluginDriver, Layer, Middleware, MiddlewareOutput, ParamDef,
	ParamType, Plugin, PluginRole, ResolvedInputs, Terminator, TerminatorResult,
};
use crate::plugins::system as drivers;
use crate::resources::kv::KvStore;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use std::any::Any;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::fs;

#[derive(Debug, Clone)]
pub struct ExternalPlugin {
	config: ExternalPluginConfig,
}

pub async fn get_trusted_bin_root() -> PathBuf {
	let root = crate::common::config::file_loader::get_config_dir().join("bin");
	fs::canonicalize(&root).await.unwrap_or(root)
}

pub async fn validate_command_path(program: &str) -> Result<PathBuf> {
	let bin_root = get_trusted_bin_root().await;
	let program_path = Path::new(program);

	let absolute_path = if program_path.is_absolute() {
		fs::canonicalize(program_path)
			.await
			.map_err(|e| anyhow!("Failed to resolve absolute path '{program}': {e}"))?
	} else {
		let joined = bin_root.join(program_path);
		fs::canonicalize(&joined)
			.await
			.map_err(|e| anyhow!("Program '{program}' not found in trusted bin directory: {e}"))?
	};

	if !absolute_path.starts_with(&bin_root) {
		return Err(anyhow!(
			"Security Violation - Program '{program}' is outside the trusted bin directory."
		));
	}

	if !fs::metadata(&absolute_path)
		.await
		.map(|m| m.is_file())
		.unwrap_or(false)
	{
		return Err(anyhow!("Path '{program}' is not a file."));
	}

	Ok(absolute_path)
}

impl ExternalPlugin {
	#[must_use]
	pub fn new(config: ExternalPluginConfig) -> Self {
		Self { config }
	}

	pub async fn validate_connectivity(&self) -> Result<()> {
		if self.config.role == PluginRole::Terminator {
			return Err(anyhow!("External plugins cannot be Terminators."));
		}

		let skip_validation = env_loader::to_lowercase(&env_loader::get_env(
			"SKIP_VALIDATE_CONNECTIVITY",
			"false".to_owned(),
		)) == "true";

		match &self.config.driver {
			ExternalPluginDriver::Http { url } => {
				if skip_validation {
					return Ok(());
				}
				let client = reqwest::Client::builder()
					.timeout(Duration::from_secs(3))
					.build()?;
				let response = client.request(reqwest::Method::OPTIONS, url).send().await?;
				if !response.status().is_success() {
					return Err(anyhow!("Endpoint returned error: {}.", response.status()));
				}
				Ok(())
			}
			ExternalPluginDriver::Unix { path } => {
				if skip_validation {
					return Ok(());
				}
				if fs::metadata(path).await.is_err() {
					return Err(anyhow!("Unix socket path does not exist: {path}"));
				}
				Ok(())
			}
			ExternalPluginDriver::Command { program, .. } => {
				validate_command_path(program).await?;
				Ok(())
			}
		}
	}
}

impl Plugin for ExternalPlugin {
	fn name(&self) -> &str {
		&self.config.name
	}
	fn params(&self) -> Vec<ParamDef> {
		self
			.config
			.params
			.iter()
			.map(|p| ParamDef {
				name: Cow::Owned(p.name.clone()),
				required: p.required,
				param_type: ParamType::String,
			})
			.collect()
	}
	fn as_any(&self) -> &dyn Any {
		self
	}
	fn as_middleware(&self) -> Option<&dyn Middleware> {
		if self.config.role == PluginRole::Middleware {
			Some(self)
		} else {
			None
		}
	}
	fn as_generic_middleware(&self) -> Option<&dyn crate::engine::interfaces::GenericMiddleware> {
		if self.config.role == PluginRole::Middleware {
			Some(self)
		} else {
			None
		}
	}
	fn as_terminator(&self) -> Option<&dyn Terminator> {
		None
	}
}

#[async_trait]
impl crate::engine::interfaces::GenericMiddleware for ExternalPlugin {
	fn output(&self) -> Vec<Cow<'static, str>> {
		if self.config.output.is_empty() {
			vec!["success".into(), "failure".into()]
		} else {
			self
				.config
				.output
				.iter()
				.map(|s| Cow::Owned(s.clone()))
				.collect()
		}
	}
	async fn execute(&self, inputs: ResolvedInputs) -> Result<MiddlewareOutput> {
		drivers::execute_driver(&self.config.driver, self.name(), inputs).await
	}
}

#[async_trait]
impl Middleware for ExternalPlugin {
	fn output(&self) -> Vec<Cow<'static, str>> {
		if self.config.output.is_empty() {
			vec!["success".into(), "failure".into()]
		} else {
			self
				.config
				.output
				.iter()
				.map(|s| Cow::Owned(s.clone()))
				.collect()
		}
	}
	async fn execute(&self, inputs: ResolvedInputs) -> Result<MiddlewareOutput> {
		drivers::execute_driver(&self.config.driver, self.name(), inputs).await
	}
}

#[async_trait]
impl Terminator for ExternalPlugin {
	fn supported_layers(&self) -> Vec<Layer> {
		vec![]
	}
	async fn execute(
		&self,
		_i: ResolvedInputs,
		_kv: &mut KvStore,
		_c: ConnectionObject,
	) -> Result<TerminatorResult> {
		Err(anyhow!("External plugins cannot be Terminators."))
	}
}