#[cfg(feature = "http-client")]
pub(crate) mod http_client;
#[cfg(feature = "http-server")]
pub(crate) mod http_server;
use crate::interfaces::{Interface, InterfaceConfig};
use crate::notifications::Notification;
use crate::{Error, CRATE_VERSION};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::path::PathBuf;
use tokio::sync::{broadcast, mpsc, watch};
use url::{ParseError, Url};
const DEFAULT_HOST: &str = "http://0.0.0.0";
const HTTP: &str = "http";
const HTTPS: &str = "https";
const DEFAULT_PORT: u16 = 8080;
const BASE_PATH: &str = "pass-it-on";
const NOTIFICATION_PATH: &str = "notification";
const VERSION_PATH: &str = "version";
#[derive(Debug, Deserialize, Serialize)]
struct Version {
version: String,
}
#[derive(Debug, Clone)]
pub struct HttpSocketInterface {
host: Url,
tls: bool,
port: u16,
tls_cert_path: Option<PathBuf>,
tls_key_path: Option<PathBuf>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
#[serde(default)]
pub(crate) struct HttpSocketConfigFile {
pub host: String,
pub tls: Option<bool>,
pub port: i64,
pub tls_cert_path: Option<String>,
pub tls_key_path: Option<String>,
}
impl Version {
fn new() -> Self {
Self { version: CRATE_VERSION.to_string() }
}
}
impl HttpSocketInterface {
pub fn new<P: AsRef<str>>(host_url: &Url, cert_path: Option<P>, key_path: Option<P>) -> Self {
let host = host_url.clone();
let tls = host.scheme().eq_ignore_ascii_case(HTTPS);
let port = host.port().unwrap_or(DEFAULT_PORT);
let tls_cert_path = cert_path.map(|p| PathBuf::from(p.as_ref()));
let tls_key_path = key_path.map(|p| PathBuf::from(p.as_ref()));
Self { host, tls, port, tls_cert_path, tls_key_path }
}
pub fn host(&self) -> &str {
self.host.as_str()
}
pub fn sockets(&self) -> Result<Vec<SocketAddr>, Error> {
Ok(self.host.socket_addrs(|| Some(self.port()))?)
}
pub fn port(&self) -> u16 {
self.port
}
pub fn tls(&self) -> bool {
self.tls
}
pub fn tls_cert_path(&self) -> &Option<PathBuf> {
&self.tls_cert_path
}
pub fn tls_key_path(&self) -> &Option<PathBuf> {
&self.tls_key_path
}
}
impl Default for HttpSocketConfigFile {
fn default() -> Self {
Self {
host: DEFAULT_HOST.into(),
tls: None,
port: DEFAULT_PORT as i64,
tls_cert_path: None,
tls_key_path: None,
}
}
}
impl Default for HttpSocketInterface {
fn default() -> Self {
Self {
host: Url::parse(DEFAULT_HOST).unwrap(),
tls: false,
port: DEFAULT_PORT,
tls_cert_path: None,
tls_key_path: None,
}
}
}
impl TryFrom<&HttpSocketConfigFile> for HttpSocketInterface {
type Error = Error;
fn try_from(value: &HttpSocketConfigFile) -> Result<Self, Self::Error> {
if !(value.port < u16::MAX as i64 && value.port > u16::MIN as i64) {
return Err(Error::invalid_port_number(value.port));
}
let mut url = parse_url(value.host.as_str())?;
if let Some(explict_tls) = value.tls {
match explict_tls {
true => url.set_scheme(HTTPS),
false => url.set_scheme(HTTP),
}
.expect("TryFrom HttpSocketConfigFile Unable to set url scheme");
}
url.set_port(Some(value.port as u16)).unwrap();
Ok(HttpSocketInterface::new(&url, value.tls_cert_path.as_ref(), value.tls_key_path.as_ref()))
}
}
#[typetag::deserialize(name = "http")]
impl InterfaceConfig for HttpSocketConfigFile {
fn to_interface(&self) -> Result<Box<dyn Interface + Send>, Error> {
Ok(Box::new(HttpSocketInterface::try_from(self)?))
}
}
#[async_trait]
impl Interface for HttpSocketInterface {
#[cfg(feature = "http-server")]
async fn receive(&self, interface_tx: mpsc::Sender<String>, shutdown: watch::Receiver<bool>) -> Result<(), Error> {
use crate::interfaces::http::http_server::start_monitoring;
if self.tls && (self.tls_cert_path().is_none() || self.tls_cert_path().is_none()) {
Err(Error::invalid_interface_configuration(
"Both tls_cert_path and tls_cert_path must be provided for a TLS server",
))
} else {
for socket in self.sockets()? {
let tls = self.tls;
let itx = interface_tx.clone();
let srx = shutdown.clone();
let cert_path = self.tls_cert_path.clone();
let key_path = self.tls_key_path.clone();
tokio::spawn(async move { start_monitoring(itx, srx, socket, tls, cert_path, key_path).await });
}
Ok(())
}
}
#[cfg(not(feature = "http-server"))]
async fn receive(
&self,
_interface_tx: mpsc::Sender<String>,
_shutdown: watch::Receiver<bool>,
) -> Result<(), Error> {
Err(Error::disabled_interface_feature("http-server".to_string()))
}
#[cfg(feature = "http-client")]
async fn send(
&self,
interface_rx: broadcast::Receiver<Notification>,
shutdown: watch::Receiver<bool>,
) -> Result<(), Error> {
use crate::interfaces::http::http_client::start_sending;
use tracing::debug;
let mut url = self.host.clone();
url.set_path(format!("{}/{}", BASE_PATH, NOTIFICATION_PATH).as_str());
debug!("Sending notification to: {}", url.as_str());
tokio::spawn(async move { start_sending(interface_rx, shutdown, url.as_str()).await });
Ok(())
}
#[cfg(not(feature = "http-client"))]
async fn send(
&self,
_interface_rx: broadcast::Receiver<Notification>,
_shutdown: watch::Receiver<bool>,
) -> Result<(), Error> {
Err(Error::disabled_interface_feature("http-client".to_string()))
}
}
fn parse_url(value: &str) -> Result<Url, Error> {
match Url::parse(value) {
Ok(url) => Ok(url),
Err(ParseError::RelativeUrlWithoutBase) => parse_url(format!("{}://{}", HTTP, value).as_str()),
Err(error) => Err(error.into()),
}
}