use log::debug; use crate::{Error, Result}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::path::PathBuf; use ipnetwork::{Ipv4Network, Ipv6Network}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] pub struct Specification { pub entrypoints: HashMap, } #[derive(Serialize, Deserialize, Debug)] pub struct Entrypoint { #[serde(default)] pub trigger: Trigger, #[serde(default = "Arg::default_vec")] pub args: Vec, #[serde(default)] pub environment: HashSet, } #[derive(Serialize, Deserialize, Debug)] pub enum Trigger { /// Start this entrypoint at application startup Startup, /// Trigger this entrypoint when a named pipe receives data Pipe(String), /// Trigger this entrypoint when a named file socket receives data FileSocket(String), } impl Default for Trigger { fn default() -> Self { Self::Startup } } #[derive(Serialize, Deserialize, PartialEq, Debug)] pub enum Arg { /// The binary name, or argv[0], of the original program start BinaryName, /// The name of this entrypoint Entrypoint, /// A file descriptor for a file on the filesystem in the launching namespace File(PathBuf), /// A chosen end of a named pipe Pipe(Pipe), /// File socket FileSocket(FileSocket), /// A value specified by the trigger /// NOTE: Only valid if the trigger is of type Pipe(...) or FileSocket(...) Trigger, /// A TCP Listener TcpListener { addr: SocketAddr }, /// An RPC socket that accepts specified commands Rpc(Vec), /// The rest of argv[1..], 0 or more arguments Trailing, } impl Arg { fn default_vec() -> Vec { vec![Arg::BinaryName] } } #[derive(Serialize, Deserialize, PartialEq, Debug)] pub enum RpcSpecification { /// Open a TCP socket /// /// None for each value means that any value is allowed in the call. /// A specified value restricts to exactly that. OpenTcpSocket { family: Option, port: Option, host: Option, }, /// Open a UDP socket /// /// None for each value means that any value is allowed in the call. /// A specified value restricts to exactly that. OpenUdpSocket { family: Option, port: Option, host: Option, }, } #[derive(Serialize, Deserialize, PartialEq, Debug)] pub enum AddressFamily { /// IPv4 address Inet, /// IPv6 address Inet6, } #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub enum Pipe { Rx(String), Tx(String), } impl Pipe { pub fn get_name(&self) -> &str { match self { Pipe::Rx(n) => n, Pipe::Tx(n) => n, } } } #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub enum FileSocket { Rx(String), Tx(String), } impl FileSocket { pub fn get_name(&self) -> &str { match self { FileSocket::Rx(n) => n, FileSocket::Tx(n) => n, } } } #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] pub enum Environment { Filesystem { host_path: PathBuf, environment_path: PathBuf, }, Hostname(String), DomainName(String), Procfs, Stdin, Stdout, Stderr, } #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] pub enum Network { InternetV4, InternetV6, PrivateV4(Ipv4Network), PrivateV6(Ipv6Network), } impl Specification { pub fn pipes(&self) -> (Vec<&str>, Vec<&str>) { let mut read = Vec::new(); let mut write = Vec::new(); for entry in self.entrypoints.values() { if let Trigger::Pipe(s) = &entry.trigger { read.push(s.as_str()); } for arg in &entry.args { if let Arg::Pipe(p) = arg { match p { Pipe::Rx(s) => read.push(s.as_str()), Pipe::Tx(s) => write.push(s.as_str()), } } } } debug!("read pipes: {:?}", &read); debug!("write pipes: {:?}", &write); (read, write) } pub fn sockets(&self) -> (Vec<&str>, Vec<&str>) { let mut read = Vec::new(); let mut write = Vec::new(); for entry in self.entrypoints.values() { if let Trigger::FileSocket(s) = &entry.trigger { read.push(s.as_str()); } for arg in &entry.args { if let Arg::FileSocket(p) = arg { match p { FileSocket::Rx(s) => read.push(s.as_str()), FileSocket::Tx(s) => write.push(s.as_str()), } } } } debug!("read sockets: {:?}", &read); debug!("write sockets: {:?}", &write); (read, write) } pub fn validate(&self) -> Result<()> { // validate pipes match let (read, write) = self.pipes(); let mut read_set = HashSet::with_capacity(read.len()); for pipe in read { if !read_set.insert(pipe) { return Err(Error::BadPipe(pipe.to_string())); } } let mut write_set = HashSet::with_capacity(write.len()); for pipe in write { if !write_set.insert(pipe) { return Err(Error::BadPipe(pipe.to_string())); } } for pipe in read_set { if !write_set.remove(pipe) { return Err(Error::BadPipe(pipe.to_string())); } } if let Some(pipe) = write_set.into_iter().next() { return Err(Error::BadPipe(pipe.to_string())); } // validate sockets match let (read, write) = self.sockets(); let mut read_set = HashSet::with_capacity(read.len()); for socket in read { if !read_set.insert(socket) { return Err(Error::BadFileSocket(socket.to_string())); } } let mut write_set = HashSet::with_capacity(write.len()); for socket in write { write_set.insert(socket); } for socket in &read_set { if !write_set.contains(socket) { return Err(Error::BadFileSocket(socket.to_string())); } } if let Some(socket) = (&write_set - &read_set).into_iter().next() { return Err(Error::BadFileSocket(socket.to_string())); } // validate trigger arguments make sense for entrypoint in self.entrypoints.values() { if entrypoint.args.contains(&Arg::Trigger) { match entrypoint.trigger { Trigger::Pipe(_) => {} Trigger::FileSocket(_) => {} _ => return Err(Error::BadTriggerArgument), } } } Ok(()) } }