diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4b796b7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "pacwrap" +version = "0.1.0" +edition = "2021" + +[dependencies] +command-fds = "0.2.2" +#tempfile = "3" +serde_json = "1.0" +envy = "0.4" +nix = "0.26.2" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +typetag="0.2" +os_pipe = "1.1.4" +signal-hook = "0.3.15" + +[profile.release] +lto = "thin" +opt-level = "s" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a987213 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,158 @@ +#![allow(unused_variables)] + +use serde::{Deserialize, Serialize}; +use std::vec::Vec; +use std::env::var; +use std::path::Path; +use std::fs::File; +use std::process::exit; +use std::collections::HashMap; + +use crate::config::filesystem::Filesystem; +use crate::config::permission::Permission; +use crate::config::permission::none::NONE; +use crate::config::filesystem::root::ROOT; +use crate::config::filesystem::home::HOME; +use crate::utils::print_error; +use crate::config::dbus::Dbus; +use crate::Arguments; + +pub use vars::InsVars; +pub mod vars; +pub mod filesystem; +pub mod permission; +pub mod dbus; + +#[derive(Serialize, Deserialize)] +pub struct Instance { + #[serde(default)] + container_type: String, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + dependencies: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + explicit_packages: Vec, + #[serde(default)] + retain_session: bool, + #[serde(default)] + allow_forking: bool, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + filesystem: Vec>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + permissions: Vec>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + dbus: Vec>, + +} + +impl Instance { + pub fn new(ctype: String, pkg: Vec, deps: Vec) -> Self { + let mut fs: Vec> = Vec::new(); + let mut per: Vec> = Vec::new(); + + fs.push(Box::new(ROOT {})); + fs.push(Box::new(HOME {})); + per.push(Box::new(NONE {})); + + Self { + container_type: ctype, + dependencies: deps, + explicit_packages: pkg, + allow_forking: false, + retain_session: false, + permissions: per, + dbus: Vec::new(), + filesystem: fs, + } + } + + fn set(&mut self, ctype: String, dep: Vec, pkg: Vec) { + self.container_type=ctype; + self.dependencies=dep; + self.explicit_packages=pkg; + } + + pub fn container_type(&self) -> &String {&self.container_type} + pub fn permissions(&self) -> &Vec> { &self.permissions } + pub fn filesystem(&self) -> &Vec> { &self.filesystem } + pub fn dbus(&self) -> &Vec> { &self.dbus } + pub fn allow_forking(&self) -> &bool { &self.allow_forking } + pub fn retain_session(&self) -> &bool { &self.retain_session } + pub fn dependencies(&self) -> &Vec { &self.dependencies } + pub fn explicit_packages(&self) -> &Vec { &self.explicit_packages } +} + + +pub fn aux_config() { + let args = Arguments::new(1, "-", HashMap::from([("--save".into(), "s".into()), + ("--load".into(),"l".into())])); + let instance = &args.get_targets()[0]; + + match args.get_switch().as_str() { + s if s.contains("s") => save_bash_configuration(instance), + s if s.contains("l") => bash_configuration(&instance), + &_ => print_error(format!("Invalid switch sequence.")), + } +} + +pub fn save_bash_configuration(ins: &String) { + let pkgs: Vec = var("PACWRAP_PKGS").unwrap().split_whitespace().map(str::to_string).collect(); + let deps: Vec = var("PACWRAP_DEPS").unwrap().split_whitespace().map(str::to_string).collect(); + let ctype = var("PACWRAP_TYPE").unwrap(); + let vars = InsVars::new(ins); + let mut instance: Instance; + let path: &str = vars.config_path().as_str(); + + match File::open(path) { + Ok(file) => instance = read_yaml(file), + Err(_) => instance = Instance::new(ctype.clone(), pkgs.clone(), deps.clone()), + } + + instance.set(ctype, deps, pkgs); + save_configuration(&instance, path.to_string()); +} + +pub fn bash_configuration(instance: &String) { + let vars = InsVars::new(instance); + let ins = &load_configuration(vars.config_path()); + let depends = ins.dependencies(); + let pkgs = ins.explicit_packages(); + let mut pkgs_string = String::new(); + let mut depends_string = String::new(); + + println!("INSTANCE_CONFIG[{},0]={}", instance, ins.container_type()); + + for i in depends.iter() { + depends_string.push_str(&format!("{} ", i)); + } + println!("INSTANCE_CONFIG[{},1]=\"{}\"", instance, depends_string); + + for i in pkgs.iter() { + pkgs_string.push_str(&format!("{} ", i)); + } + println!("INSTANCE_CONFIG[{},3]=\"{}\"", instance, pkgs_string); +} + +pub fn save_configuration(ins: &Instance, config_path: String) { + let f = File::create(Path::new(&config_path)).expect("Couldn't open file"); + serde_yaml::to_writer(f, &ins).unwrap(); +} + + +fn read_yaml(file: File) -> Instance { + match serde_yaml::from_reader(file) { + Ok(file) => return file, + Err(error) => { + print_error(format!("{}", error)); + exit(2); + } + } +} + + +pub fn load_configuration(config_path: &String) -> Instance { + let path: &str = config_path.as_str(); + match File::open(path) { + Ok(file) => read_yaml(file), + Err(_) => Instance::new(format!("BASE"), Vec::new(), Vec::new()), + } +} diff --git a/src/config/dbus.rs b/src/config/dbus.rs new file mode 100644 index 0000000..d3f48f0 --- /dev/null +++ b/src/config/dbus.rs @@ -0,0 +1,11 @@ +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; + +mod socket; +mod appindicator; +mod xdg_portal; + +#[typetag::serde(tag = "permission")] +pub trait Dbus { + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars); +} diff --git a/src/config/dbus/appindicator.rs b/src/config/dbus/appindicator.rs new file mode 100644 index 0000000..ff45bd0 --- /dev/null +++ b/src/config/dbus/appindicator.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Dbus}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct APPINDICATOR; + +#[typetag::serde] +impl Dbus for APPINDICATOR { + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.dbus("broadcast", "org.kde.StatusNotifierWatcher=@/StatusNotifierWatcher"); + } +} diff --git a/src/config/dbus/socket.rs b/src/config/dbus/socket.rs new file mode 100644 index 0000000..0cbb55c --- /dev/null +++ b/src/config/dbus/socket.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Dbus}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SOCKET { + socket: String, + address: Vec +} + +#[typetag::serde] +impl Dbus for SOCKET { + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + match self.socket.to_lowercase().as_str() { + p if p == "call" || p == "talk" || p == "see" || p == "own" || p == "broadcast" => { + for sock in self.address.iter() { + args.dbus(p, sock); + } + }, + &_ => {} + } + } +} + diff --git a/src/config/dbus/xdg_portal.rs b/src/config/dbus/xdg_portal.rs new file mode 100644 index 0000000..c3c4e79 --- /dev/null +++ b/src/config/dbus/xdg_portal.rs @@ -0,0 +1,17 @@ +#![allow(non_camel_case_types)] + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Dbus}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct XDG_PORTAL; + +#[typetag::serde] +impl Dbus for XDG_PORTAL { + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.dbus("call", "org.freedesktop.portal.*=*"); + args.dbus("broadcast", "org.freedesktop.portal.*=@/org/freedesktop/portal/*"); + } +} diff --git a/src/config/filesystem.rs b/src/config/filesystem.rs new file mode 100644 index 0000000..bbc0279 --- /dev/null +++ b/src/config/filesystem.rs @@ -0,0 +1,34 @@ +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; + +pub mod home; +pub mod root; +mod to_home; +mod to_root; +mod dir; + +pub struct Error { + error: String, + mod_name: String, + critical: bool +} + +impl Error { + pub fn new(name: impl Into, err: impl Into, critical: bool) -> Self { + Self { error: err.into(), mod_name: name.into(), critical: critical } + } + + pub fn error(&self) -> &String { &self.error } + pub fn module(&self) -> &String { &self.mod_name } + pub fn critical(&self) -> &bool { &self.critical } +} + +#[typetag::serde(tag = "mount")] +pub trait Filesystem { + fn check(&self, vars: &InsVars) -> Result<(), Error>; + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars); +} + +fn default_permission() -> String { + "ro".into() +} diff --git a/src/config/filesystem/dir.rs b/src/config/filesystem/dir.rs new file mode 100644 index 0000000..2fab945 --- /dev/null +++ b/src/config/filesystem/dir.rs @@ -0,0 +1,30 @@ +#![allow(non_camel_case_types)] + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; +use crate::config::filesystem::{Filesystem, Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NEW_DIR { + #[serde(default)] + path: Vec +} + +#[typetag::serde] +impl Filesystem for NEW_DIR { + fn check(&self, vars: &InsVars) -> Result<(), Error> { + if self.path.len() == 0 { + Err(Error::new("DIR", format!("Path not specified."), false))? + } + + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + for dir in self.path.iter() { + args.dir(dir); + } + } +} diff --git a/src/config/filesystem/home.rs b/src/config/filesystem/home.rs new file mode 100644 index 0000000..2aad083 --- /dev/null +++ b/src/config/filesystem/home.rs @@ -0,0 +1,26 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; +use crate::config::filesystem::{Filesystem, Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HOME; + +#[typetag::serde] +impl Filesystem for HOME { + fn check(&self, vars: &InsVars) -> Result<(), Error> { + if ! Path::new(vars.home()).exists() { + Err(Error::new("HOME", format!("INSTANCE_HOME not found."), true))? + } + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.bind(vars.home(), vars.home_mount()); + args.env("HOME", vars.home_mount()); + args.env("USER", vars.user()); + } +} diff --git a/src/config/filesystem/root.rs b/src/config/filesystem/root.rs new file mode 100644 index 0000000..5990c28 --- /dev/null +++ b/src/config/filesystem/root.rs @@ -0,0 +1,29 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; +use crate::config::filesystem::{Filesystem, Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ROOT; + +#[typetag::serde] +impl Filesystem for ROOT { + fn check(&self, vars: &InsVars) -> Result<(), Error> { + if ! Path::new(vars.root()).exists() { + Err(Error::new("ROOT", format!("Container {} not found. ", vars.instance()), true))? + } + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.robind(format!("{}/usr", vars.root()), "/usr"); + args.robind(format!("{}/etc", vars.root()), "/etc"); + args.symlink("/usr/lib", "/lib"); + args.symlink("/usr/lib", "/lib64"); + args.symlink("/usr/bin", "/bin"); + args.symlink("/usr/bin", "/sbin"); + } +} diff --git a/src/config/filesystem/to_home.rs b/src/config/filesystem/to_home.rs new file mode 100644 index 0000000..5ed982b --- /dev/null +++ b/src/config/filesystem/to_home.rs @@ -0,0 +1,54 @@ +#![allow(non_camel_case_types)] + +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::constants::return_home; +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; +use crate::config::filesystem::{Filesystem, Error, default_permission}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TO_HOME { + #[serde(default = "default_permission")] + permission: String, + #[serde(default)] + path: Vec +} +#[typetag::serde] +impl Filesystem for TO_HOME { + fn check(&self, vars: &InsVars) -> Result<(), Error> { + let per = self.permission.to_lowercase(); + + if per != "ro" && per != "rw" { + Err(Error::new("TO_HOME", format!("{} is an invalid permission.", self.permission), true))? + } + + if self.path.len() == 0 { + Err(Error::new("TO_HOME", format!("Path not specified."), false))? + } + + if ! Path::new(&format!("{}/{}", return_home(), &self.path[0])).exists() { + Err(Error::new("TO_HOME", format!("~/{} not found.", self.path[0]), true))? + } + + Ok(()) + } + + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + let src = &self.path[0]; + let mut dest: &String = src; + + if self.path.len() > 1 { dest = &self.path[1]; } + + let path_src = format!("{}/{}", return_home(), &self.path[0]); + let path_dest = format!("{}/{}", vars.home_mount(), dest); + + match self.permission.to_lowercase().as_str() { + p if p == "rw" => args.bind(path_src, path_dest), + &_ => args.robind(path_src, path_dest) + } + } +} diff --git a/src/config/filesystem/to_root.rs b/src/config/filesystem/to_root.rs new file mode 100644 index 0000000..4fd3b59 --- /dev/null +++ b/src/config/filesystem/to_root.rs @@ -0,0 +1,49 @@ +#![allow(non_camel_case_types)] +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::InsVars; +use crate::config::filesystem::{Filesystem, Error, default_permission}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TO_ROOT { + #[serde(default = "default_permission")] + permission: String, + #[serde(default)] + path: Vec +} +#[typetag::serde] +impl Filesystem for TO_ROOT { + + fn check(&self, vars: &InsVars) -> Result<(), Error> { + let per = self.permission.to_lowercase(); + + if per != "ro" && per != "rw" { + Err(Error::new("TO_ROOT", format!("{} is an invalid permission.", self.permission), true))? + } + + if self.path.len() == 0 { + Err(Error::new("TO_ROOT", format!("Path not specified."), false))? + } + + if ! Path::new(&self.path[0]).exists() { + Err(Error::new("TO_ROOT", format!("Source path not found."), true))? + } + + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + let src = &self.path[0]; + let mut dest: &String = src; + + if self.path.len() > 1 { dest = &self.path[1]; } + + match self.permission.to_lowercase().as_str() { + p if p == "rw" => args.bind(src, dest), + &_ => args.robind(src, dest) + } + } +} diff --git a/src/config/permission.rs b/src/config/permission.rs new file mode 100644 index 0000000..8b2df73 --- /dev/null +++ b/src/config/permission.rs @@ -0,0 +1,31 @@ +use crate::exec::args::ExecutionArgs; +use crate::config::vars::InsVars; + +pub mod none; +mod x11; +mod env; +mod pulseaudio; +mod pipewire; +mod gpu; +mod net; +mod dev; + +pub struct Error { + error: String, + mod_name: String +} + +impl Error { + pub fn new(name: impl Into, err: impl Into) -> Self { + Self { error: err.into(), mod_name: name.into() } + } + + pub fn error(&self) -> &String { &self.error } + pub fn module(&self) -> &String { &self.mod_name } +} + +#[typetag::serde(tag = "permission")] +pub trait Permission { + fn check(&self) -> Result<(),Error>; + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars); +} diff --git a/src/config/permission/dev.rs b/src/config/permission/dev.rs new file mode 100644 index 0000000..8023f00 --- /dev/null +++ b/src/config/permission/dev.rs @@ -0,0 +1,26 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct DEV { + device: String +} + +#[typetag::serde] +impl Permission for DEV { + fn check(&self) -> Result<(),Error> { + if ! Path::new(&format!("/dev/{}",self.device)).exists() { + Err(Error::new("dev", format!("/dev/{} is inaccessible.", self.device)))? + } + + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.dev(&format!("/dev/{}", self.device)); + } +} diff --git a/src/config/permission/env.rs b/src/config/permission/env.rs new file mode 100644 index 0000000..54baf1e --- /dev/null +++ b/src/config/permission/env.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use std::env; + +use crate::exec::args::ExecutionArgs; +use crate::utils::print_warning; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ENV { + var: String, + #[serde(default = "default_set")] + set: String +} + +#[typetag::serde] +impl Permission for ENV { + + fn check(&self) -> Result<(),Error> { + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + let mut set = self.set.to_owned(); + if set == "" { + match env::var(&self.var) { + Ok(env) => set = env, + Err(_) => { print_warning(format!("Environment variable {} is empty.", &self.var)) + } + } + } + args.env(&self.var, set); + } +} + +fn default_set() -> String { + "".into() +} + diff --git a/src/config/permission/gpu.rs b/src/config/permission/gpu.rs new file mode 100644 index 0000000..6e51e03 --- /dev/null +++ b/src/config/permission/gpu.rs @@ -0,0 +1,38 @@ +use std::fs::read_dir; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct GPU; + +#[typetag::serde] +impl Permission for GPU { + fn check(&self) -> Result<(),Error> { + if ! Path::new("/dev/").exists() { + Err(Error::new("GPU", format!("/dev is inaccessible.")))? + } + + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + for e in read_dir("/dev/").unwrap() { + match e { + Ok(e) => { + let file = e.file_name(); + let dev = file.to_str().unwrap(); + match dev { + p if p.starts_with("nvidia") => args.dev(&format!("/dev/{}",dev)), + p if p == "dri" => args.dev(&format!("/dev/{}",dev)), + &_ => {} + } + } + Err(_) => continue, + } + } + } +} diff --git a/src/config/permission/net.rs b/src/config/permission/net.rs new file mode 100644 index 0000000..462885b --- /dev/null +++ b/src/config/permission/net.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NET; + +#[typetag::serde] +impl Permission for NET { + fn check(&self) -> Result<(),Error> { + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.push_env("--share-net"); + args.bind("/etc/resolv.conf", "/etc/resolv.conf"); + } +} diff --git a/src/config/permission/none.rs b/src/config/permission/none.rs new file mode 100644 index 0000000..f9f5812 --- /dev/null +++ b/src/config/permission/none.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NONE; + +#[typetag::serde] +impl Permission for NONE { + + fn check(&self) -> Result<(),Error> { + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) {} +} diff --git a/src/config/permission/pipewire.rs b/src/config/permission/pipewire.rs new file mode 100644 index 0000000..f129871 --- /dev/null +++ b/src/config/permission/pipewire.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PIPEWIRE { + #[serde(skip_serializing_if = "is_default_socket", default = "default_socket")] + socket: String, +} + +#[typetag::serde] +impl Permission for PIPEWIRE { + fn check(&self) -> Result<(),Error> { + if ! Path::new(&self.socket).exists() { + Err(Error::new("PIPEWIRE", format!("Pipewire socket not present.")))? + } + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.robind(&self.socket, default_socket()); + } +} + +fn is_default_socket(var: &String) -> bool { + let default: &String = &default_socket(); + default == var +} + +fn default_socket() -> String { + format!("/run/user/{}/pipewire-0", nix::unistd::geteuid()) +} diff --git a/src/config/permission/pulseaudio.rs b/src/config/permission/pulseaudio.rs new file mode 100644 index 0000000..8dae5ef --- /dev/null +++ b/src/config/permission/pulseaudio.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PULSEAUDIO { + #[serde(skip_serializing_if = "is_default_socket", default = "default_socket")] + socket: String, +} + +#[typetag::serde] +impl Permission for PULSEAUDIO { + fn check(&self) -> Result<(),Error> { + if ! Path::new(&self.socket).exists() { + Err(Error::new("PULSEAUDIO", format!("Pulseaudio socket not present.")))? + } + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + args.robind(&self.socket, default_socket()); + } +} + +fn is_default_socket(var: &String) -> bool { + let default: &String = &default_socket(); + default == var +} + +fn default_socket() -> String { + format!("/run/user/{}/pulse", nix::unistd::geteuid()) +} diff --git a/src/config/permission/x11.rs b/src/config/permission/x11.rs new file mode 100644 index 0000000..8e72251 --- /dev/null +++ b/src/config/permission/x11.rs @@ -0,0 +1,52 @@ +use std::path::Path; +use std::env::var; + +use serde::{Deserialize, Serialize}; + +use crate::exec::args::ExecutionArgs; +use crate::config::{InsVars, Permission, permission::Error}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct X11 { + #[serde(skip_serializing_if = "is_default", default = "default_display")] + display: i8, +} + +#[typetag::serde] +impl Permission for X11 { + fn check(&self) -> Result<(),Error> { + if ! Path::new(&format!("/tmp/.X11-unix/X{}", self.display)).exists() { + Err(Error::new("X11", format!("Diaplay server :{} is not running.", self.display)))? + } + + match var("XAUTHORITY") { + Ok(env) => { + if env == "" { + Err(Error::new("X11", format!("XAUTHORITY is unset.")))? + } + if ! Path::new(&env).exists() { + Err(Error::new("X11", format!("XAUTHORITY path is invalid.")))? + } + }, + Err(_) => Err(Error::new("X11", format!("XAUTHORITY is unset.")))? + } + Ok(()) + } + + fn register(&self, args: &mut ExecutionArgs, vars: &InsVars) { + let xauth = var("XAUTHORITY").unwrap(); + let container_xauth = format!("/run/user/{}/Xauthority", nix::unistd::geteuid()); + let display = format!(":{}", self.display); + args.env("DISPLAY", display); + args.robind(xauth, &container_xauth); + args.env("XAUTHORITY", container_xauth); + } +} + +fn is_default(var: &i8) -> bool { + var == &0 +} + +fn default_display() -> i8 { + 0 +} diff --git a/src/config/vars.rs b/src/config/vars.rs new file mode 100644 index 0000000..91c9d0d --- /dev/null +++ b/src/config/vars.rs @@ -0,0 +1,98 @@ +use std::env::var; + +use crate::constants::{PACWRAP_DATA_DIR, PACWRAP_CACHE_DIR, PACWRAP_CONFIG_DIR}; +use crate::config::Instance; + +pub struct LocationVars { + pub data: String, + pub cache: String, + pub conf: String, + pub home: String +} + +impl LocationVars { + pub fn new() -> Self { + let home_dir = var("HOME").unwrap(); + + let mut dir = Self { + data: format!("{}{}", &home_dir, PACWRAP_DATA_DIR), + cache: format!("{}{}", &home_dir, PACWRAP_CACHE_DIR), + conf: format!("{}{}", &home_dir, PACWRAP_CONFIG_DIR), + home: home_dir + }; + + if let Ok(var) = var("PACWRAP_DATA_DIR") { dir.data=var; } + if let Ok(var) = var("PACWRAP_CACHE_DIR") { dir.cache=var; } + if let Ok(var) = var("PACWRAP_CONFIG_DIR") { dir.conf=var; } + + dir + } +} + +#[derive(Clone)] +pub struct InsVars { + home: String, + root: String, + user: String, + config: String, + instance: String, + home_mount: String, + pub pacman_sync: String, + pub pacman_cache: String, + pub pacman_gnupg: String, + pub pacman_mirrorlist: String, + sync: String, + syncdb: String +} + +impl InsVars { + pub fn new(_i: impl Into) -> Self { + let ins = _i.into(); + let dir = LocationVars::new(); + + let mut vars = Self { + home: format!("{}/home/{}", dir.data, ins), + root: format!("{}/root/{}", dir.data, ins), + pacman_gnupg: format!("{}/pacman/gnupg", dir.data), + pacman_sync: format!("{}/pacman/sync", dir.data), + pacman_cache: format!("{}/pkg", dir.cache), + pacman_mirrorlist: format!("{}/pacman.d/mirrorlist", dir.conf), + sync: format!("{}/pacman/sync/pacman.{}.conf", dir.conf, ins), + syncdb: format!("{}/pacman/syncdb/pacman.{}.conf", dir.conf, ins), + config: format!("{}/instance/{}.yml", dir.conf, ins), + home_mount: format!("/home/{}", ins), + user: ins.clone(), + instance: ins.clone(), + }; + + if let Ok(var) = var("PACWRAP_HOME") { vars.home=var; } + + vars + } + + pub fn debug(&self, cfg: &Instance, switch: &String, runtime: &Vec) { + print!("Arguments: "); for arg in runtime.iter() { print!("{} ", arg); } println!(); + println!("Switch: -{}", switch); + println!("Instance: {}", self.instance); + println!("User: {}", var("USER").unwrap()); + println!("Home: {}", var("HOME").unwrap()); + println!("allow_forking: {}", cfg.allow_forking()); + println!("retain_session: {}", cfg.retain_session()); + println!("Config: {}", self.config); + println!("INSTANCE_USER: {}", self.user); + println!("INSTANCE_ROOT: {}", self.root); + println!("INSTANCE_HOME: {}", self.home); + println!("INSTANCE_HOME_MOUNT: {}", self.home_mount); + println!("INSTANCE_SYNC: {}", self.sync); + println!("INSTANCE_SYNCDB: {}", self.syncdb); + } + + pub fn sync(&self) -> &String { &self.sync } + pub fn syncdb(&self) -> &String { &self.syncdb } + pub fn config_path(&self) -> &String { &self.config } + pub fn root(&self) -> &String { &self.root } + pub fn home(&self) -> &String { &self.home } + pub fn home_mount(&self) -> &String { &self.home_mount } + pub fn user(&self) -> &String { &self.user } + pub fn instance(&self) -> &String { &self.instance } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..5b10718 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,8 @@ +use std::env::var; + +pub const BWRAP_EXECUTABLE: &str = "bwrap"; +pub const PACWRAP_CONFIG_DIR: &str = "/.config/pacwrap"; +pub const PACWRAP_DATA_DIR: &str = "/.local/share/pacwrap"; +pub const PACWRAP_CACHE_DIR: &str = "/.cache/pacwrap"; + +pub fn return_home() -> String { var("HOME").unwrap() } diff --git a/src/exec.rs b/src/exec.rs new file mode 100644 index 0000000..b4757ae --- /dev/null +++ b/src/exec.rs @@ -0,0 +1,272 @@ +#![allow(unused_assignments)] + +use std::env; +use std::process; +use std::process::{Command, Child, ExitStatus, exit}; +use std::{thread, time::Duration}; +use std::os::unix::io::AsRawFd; +use std::fs::{File, remove_file}; +use std::io::{Read}; +use std::path::Path; +use std::vec::Vec; +use std::collections::HashMap; + +use signal_hook::{consts::SIGINT, iterator::Signals}; +use os_pipe::{PipeReader, PipeWriter}; +use std::cell::RefCell; +use command_fds::{CommandFdExt, FdMapping}; +use serde_json::Value; + +use crate::config::{self, Instance, vars::InsVars, filesystem::Filesystem, permission::Permission, dbus::Dbus}; +use crate::utils::{print_error, print_warning, test_root, arguments::Arguments}; +use crate::constants::BWRAP_EXECUTABLE; +use crate::exec::args::ExecutionArgs; + +pub mod args; + +pub fn execute() { + let args = Arguments::new(1, "-E", HashMap::from([("--exec".into(), "E".into()), + ("--root".into(), "r".into()), + ("--shell".into(), "s".into()), + ("--command".into(),"c".into()),])); + + let switch = args.get_switch(); + let instance = args.get_targets()[0].clone(); + let runtime = args.get_runtime(); + + let instance_vars = InsVars::new(&instance); + let cfg = config::load_configuration(&instance_vars.config_path()); + + if switch.contains("v") { instance_vars.debug(&cfg, &switch, &runtime); } + + match switch.as_str() { + s if s.contains("rc") || s.contains("cr") => execute_fakeroot(instance_vars, runtime), + s if s.contains("rs") || s.contains("sr") => execute_fakeroot(instance_vars, &["bash".into()].to_vec()), + s if s.contains("s") => execute_container(instance_vars,&["bash".into()].to_vec(), cfg, switch), + &_ => execute_container(instance_vars, runtime, cfg, switch), + } +} + +fn execute_container(vars: InsVars, arguments: &Vec, cfg: Instance, switch: &String) { + let mut exec_args = ExecutionArgs::new(&["--tmpfs".into(), "/tmp".into()], + &["--dev".into(), "/dev".into(), "--proc".into(), "/proc".into()], &[]); + let mut jobs: Vec> = Vec::new(); + + if ! cfg.allow_forking() { exec_args.push_env("--die-with-parent"); } + if ! cfg.retain_session() { exec_args.push_env("--new-session"); } + if switch.contains("s") { exec_args.env("TERM", "xterm"); } + if cfg.dbus().len() > 0 { + jobs.push(register_dbus(cfg.dbus(), &vars, &mut exec_args).into()); } + + register_filesystems(cfg.filesystem(), &vars, &mut exec_args); + register_permissions(cfg.permissions(), &vars, &mut exec_args); + + //TODO: Implement separate abstraction for path vars. + + exec_args.env("PATH", "/usr/bin/:/bin"); + exec_args.env("XDG_RUNTIME_DIR", format!("/run/user/{}/", nix::unistd::geteuid())); + + if switch.contains("v") { println!("{:?} ",exec_args); } + + let (reader, writer) = os_pipe::pipe().unwrap(); + let fd = writer.as_raw_fd(); + let mut proc = Command::new(BWRAP_EXECUTABLE); + + proc.args(exec_args.get_bind()).args(exec_args.get_dev()) + .arg("--proc").arg("/proc").arg("--unshare-all").arg("--clearenv") + .arg("--info-fd").arg(fd.to_string()) + .args(exec_args.get_env()).args(arguments) + .fd_mappings(vec![FdMapping { parent_fd: fd, child_fd: fd }]).unwrap(); + + match proc.spawn() { + Ok(c) => wait_on_process(c, &read_info_json(reader, writer), *cfg.allow_forking(), jobs), + Err(_) => print_error(format!("Failed to initialise bwrap.")) + } +} + +fn read_info_json(mut reader: PipeReader, writer: PipeWriter) -> Value { + let mut output = String::new(); + drop(writer); + reader.read_to_string(&mut output).unwrap(); + match serde_json::from_str(&output) { + Ok(value) => value, + Err(_) => Value::Null + } +} + +fn signal_init(child_id: String) { + let mut signals = Signals::new(&[SIGINT]).unwrap(); + thread::spawn(move || { + for _sig in signals.forever() { + if Path::new(&format!("/proc/{}/", &child_id)).exists() { + Command::new("/usr/bin/kill").arg("-9").arg(&child_id).output().expect("Failed."); + } + clean_up_socket(); + Command::new("/usr/bin/reset").arg("-w").output().expect("Failed."); + println!(); + } + }); +} + +fn wait_on_process(mut process: Child, value: &Value, block: bool, jobs: Vec>) { + if block { + signal_init(value["child-pid"].to_string()); + } + + match process.wait() { + Ok(status) => { + if block { + let proc = format!("/proc/{}/", value["child-pid"]); + + while Path::new(&proc).exists() { + thread::sleep(Duration::from_millis(250)); + } + } + + if jobs.len() > 0 { + for job in jobs.iter() { + let mut child = job.borrow_mut(); + let _ = child.kill(); + } + } + + clean_up_socket(); + process_exit(status); + }, + Err(_) => { + print_error(format!("bwrap process abnormally terminated.")); + exit(2); + } + } +} + +fn process_exit(status: ExitStatus) { + match status.code() { + Some(o) => exit(o), + None => { + println!(); + eprintln!("bwrap process {}", status); + exit(2); + } + } +} + +fn register_filesystems(per: &Vec>, vars: &InsVars, args: &mut ExecutionArgs) { + for p in per.iter() { + match p.check(vars) { + Ok(_) => p.register(args, vars), + Err(e) => { + if *e.critical() { + print_error(format!("Failed to mount {}: {} ", e.module(), e.error())); + exit(1); + } else { + print_warning(format!("Failed to mount {}: {} ", e.module(), e.error())); + } + } + } + } +} + +fn register_permissions(per: &Vec>, vars: &InsVars, args: &mut ExecutionArgs) { + for p in per.iter() { + match p.check() { + Ok(_) => p.register(args, vars), + Err(e) => print_warning(format!("Failed to register permission {}: {} ", e.module(), e.error())) + } + } +} + +fn register_dbus(per: &Vec>, vars: &InsVars, args: &mut ExecutionArgs) -> Child { + for p in per.iter() { + p.register(args, vars); + } + + let dbus_socket_path = format!("/run/user/{}/bus", nix::unistd::geteuid()); + let dbus_socket = create_dbus_socket(); + let dbus_session = env!("DBUS_SESSION_BUS_ADDRESS", "Failure"); + + match Command::new("xdg-dbus-proxy") + .arg(dbus_session).arg(&dbus_socket) + .args(args.get_dbus()).spawn() { + Ok(child) => { + args.robind(dbus_socket, &dbus_socket_path); + args.symlink(&dbus_socket_path, "/run/dbus/system_bus_socket"); + args.env("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", &dbus_socket_path)); + child + }, + Err(_) => { + print_error("Activation of xdg-dbus-proxy failed.".into()); + exit(2); + }, + } +} + + +fn create_dbus_socket() -> String { + let socket_address = format!("/run/user/1000/pacwrap_dbus_{}", &process::id()); + + match File::create(&socket_address) { + Ok(file) => { + drop(file); + socket_address + }, + Err(_) => { + print_error(format!("Failed to create dbus socket.")); + eprintln!("Ensure you have write permissions to /run/user/."); + String::new() + } + } +} + +fn clean_up_socket() { + let socket_address = format!("/run/user/1000/pacwrap_dbus_{}", &process::id()); + + if ! Path::new(&socket_address).exists() { + return; + } + + if let Err(_) = remove_file(socket_address) { + print_error(format!("Failed to remove FD.")); + } +} + +fn execute_fakeroot(instance: InsVars, arguments: &Vec) { + test_root(&instance); + + match Command::new(BWRAP_EXECUTABLE) + .arg("--tmpfs").arg("/tmp") + .arg("--bind").arg(&instance.root()).arg("/") + .arg("--ro-bind").arg("/usr/lib/libfakeroot").arg("/usr/lib/libfakeroot/") + .arg("--ro-bind").arg("/usr/bin/fakeroot").arg("/usr/bin/fakeroot") + .arg("--ro-bind").arg("/usr/bin/fakechroot").arg("/usr/bin/fakechroot") + .arg("--ro-bind").arg("/usr/bin/faked").arg("/usr/bin/faked") + .arg("--ro-bind").arg("/etc/resolv.conf").arg("/etc/resolv.conf") + .arg("--ro-bind").arg("/etc/localtime").arg("/etc/localtime") + .arg("--bind").arg(&instance.pacman_sync).arg("/var/lib/pacman/sync") + .arg("--bind").arg(&instance.pacman_gnupg).arg("/etc/pacman.d/gnupg") + .arg("--bind").arg(&instance.pacman_cache).arg("/var/cache/pacman/pkg") + .arg("--ro-bind").arg(&instance.pacman_mirrorlist).arg("/etc/pacman.d/mirrorlist") + .arg("--ro-bind").arg(&instance.sync()).arg("/etc/pacman.conf") + .arg("--ro-bind").arg(&instance.syncdb()).arg("/tmp/pacman.conf") + .arg("--bind").arg(&instance.home()).arg(&instance.home_mount()) + .arg("--dev").arg("/dev") + .arg("--proc").arg("/proc") + .arg("--unshare-all").arg("--share-net") + .arg("--clearenv") + .arg("--hostname").arg("FakeChroot") + .arg("--new-session") + .arg("--setenv").arg("TERM").arg("xterm") + .arg("--setenv").arg("PATH").arg("/usr/bin") + .arg("--setenv").arg("CWD").arg(&instance.home_mount()) + .arg("--setenv").arg("HOME").arg(&instance.home_mount()) + .arg("--setenv").arg("USER").arg(&instance.user()) + .arg("--die-with-parent") + .arg("fakechroot") + .arg("fakeroot") + .args(arguments) + .spawn() { + Ok(process) => wait_on_process(process, &Value::Null, false, Vec::new()), + Err(_) => print_error(format!("Failed to initialise bwrap.")), + } +} + diff --git a/src/exec/args.rs b/src/exec/args.rs new file mode 100644 index 0000000..0c062f5 --- /dev/null +++ b/src/exec/args.rs @@ -0,0 +1,63 @@ +#[derive(Debug)] +pub struct ExecutionArgs { + bind: Vec, + dev: Vec, + envir: Vec, + dbus: Vec +} + + +impl ExecutionArgs { + + pub fn new(b: &[String], d: &[String], e: &[String]) -> Self { + Self { bind: b.to_vec(), dev: d.to_vec(), envir: e.to_vec(), dbus: Vec::new() } + } + + + pub fn get_bind(&self) -> &Vec { &self.bind } + pub fn get_dev(&self) -> &Vec { &self.dev } + pub fn get_env(&self) -> &Vec { &self.envir } + pub fn get_dbus(&self) -> &Vec { &self.dbus } + pub fn push_env(&mut self, src: impl Into) { self.envir.push(src.into()); } + + pub fn dir(&mut self, dest: impl Into) { + self.bind.push("--dir".into()); + self.bind.push(dest.into()); + } + + pub fn bind(&mut self, src: impl Into, dest: impl Into) { + self.bind.push("--bind".into()); + self.bind.push(src.into()); + self.bind.push(dest.into()); + } + + pub fn robind(&mut self, src: impl Into, dest: impl Into) { + self.bind.push("--ro-bind".into()); + self.bind.push(src.into()); + self.bind.push(dest.into()); + } + + pub fn symlink(&mut self, src: impl Into, dest: impl Into) { + self.bind.push("--symlink".into()); + self.bind.push(src.into()); + self.bind.push(dest.into()); + } + + pub fn env(&mut self, src: impl Into, dest: impl Into) { + self.envir.push("--setenv".into()); + self.envir.push(src.into()); + self.envir.push(dest.into()); + } + + pub fn dev(&mut self, src: impl Into) { + let dev = src.into(); + self.dev.push("--dev-bind-try".into()); + self.dev.push(dev.clone()); + self.dev.push(dev.clone()); + } + + pub fn dbus(&mut self, per: impl Into, socket: impl Into) { + self.dbus.push("--".to_owned()+&per.into()+"="+&socket.into()); + } + +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..90281ee --- /dev/null +++ b/src/main.rs @@ -0,0 +1,59 @@ +use std::env; +use std::process::Command; +use std::collections::HashMap; +use utils::arguments::Arguments; +use utils::print_help_msg; + +mod config; +mod exec; +mod constants; +mod utils; + +fn main() { + let args = Arguments::new(0, "-", HashMap::from([("--exec".into(), "E".into()), + ("--explicit".into(), "E".into()), + ("--pacman".into(),"E".into()), + ("--gen-cfg".into(),"Axc".into()), + ("--version".into(),"V".into()), + ("--create".into(),"C".into()), + ("--sync".into(),"S".into()), + ("--help".into(),"h".into()), + ("--utils".into(),"U".into()), + ("--process".into(),"P".into()),])); + + match args.get_switch().as_str() { + s if s.starts_with("E") => exec::execute(), + s if s.starts_with("V") => print_version(), + s if s.starts_with("S") => execute_pacwrap_bash("pacwrap-sync".to_string()), + s if s.starts_with("U") => execute_pacwrap_bash("pacwrap-utils".to_string()), + s if s.starts_with("C") => execute_pacwrap_bash("pacwrap-create".to_string()), + s if s.starts_with("P") => execute_pacwrap_bash("pacwrap-ps".to_string()), + s if s.starts_with("v") => execute_pacwrap_bash("pacwrap-man".to_string()), + s if s.starts_with("h") => execute_pacwrap_bash("pacwrap-man".to_string()), + s if s.starts_with("Axc") => config::aux_config(), + &_ => { + let mut ar = String::new(); + for arg in env::args().skip(1).collect::>().iter() { + ar.push_str(&format!("{} ", &arg)); + } + print_help_msg(&format!("Invalid arguments -- {}", ar)); + } + } +} + +fn print_version() { + println!("{} {}{} ", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION_MINOR")); + let info=concat!("Copyright (C) 2023 Xavier R.M.\n\n", + "Website: https://git.sapphirus.org/pacwrap\n", + "Github: https://github.com/sapphirusberyl/pacwrap\n\n", + "This program may be freely redistributed under\n", + "the terms of the GNU General Public License v3.\n"); + print!("{}", info); +} + +fn execute_pacwrap_bash(executable: String) { + let mut process = Command::new(&executable) + .args(env::args().skip(1).collect::>()) + .spawn().expect("Command failed."); + process.wait().expect("failed to wait on child"); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..70b5c43 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,51 @@ +use std::path::Path; +use std::process::exit; +use nix::unistd::isatty; + + +use crate::config::vars::InsVars; + +pub mod arguments; + +pub fn test_root(instance: &InsVars) { + if ! Path::new(&instance.root()).exists() || ! Path::new(&instance.home()).exists() { + print_error(format!("Target container {}: not found.", instance.instance())); + exit(2); + } +} + +fn support_ansi_escape(fd: i32) -> bool { + match isatty(fd) { + Ok(b) => { + if b && env!("TERM") != "dumb" { + return true; + } else { + return false; + } + }, + Err(_) => return false + } +} + +pub fn print_warning(message: String) { + if support_ansi_escape(2) { + eprintln!("warning: {}", &message); + } else { + eprintln!("WARNING: {}", &message); + } +} + +pub fn print_error(message: String) { + if support_ansi_escape(2) { + eprintln!("error: {}", &message); + } else { + eprintln!("ERROR: {}", &message); + } +} + + +pub fn print_help_msg(args: &str) { + println!("pacwrap error: {} ", args); + println!("Try 'pacwrap -h' for more information on valid operational parameters."); + exit(1); +} diff --git a/src/utils/arguments.rs b/src/utils/arguments.rs new file mode 100644 index 0000000..229ef7c --- /dev/null +++ b/src/utils/arguments.rs @@ -0,0 +1,48 @@ +use std::env; +use std::collections::HashMap; + +use crate::utils; + +#[derive(Clone)] +pub struct Arguments { + prefix: String, + switch: String, + runtime: Vec, + targets: Vec, + target: usize, + argument_map: HashMap +} + +impl Arguments { + pub fn new(amt: usize, arg: impl Into, arg_map: HashMap) -> Self { + let mut arguments = Self { + prefix: arg.into(), + switch: String::new(), + targets: Vec::new(), + runtime: Vec::new(), + target: amt, + argument_map: arg_map, + }; + arguments.parse_arguments(); + return arguments; + } + + fn parse_arguments(&mut self) { + for string in env::args().skip(1) { + match string { + string if self.argument_map.contains_key(&string) => self.append_switch(self.argument_map.get(&string).unwrap().clone()), + string if string.starts_with(self.get_prefix()) => self.append_switch(&string[self.get_prefix().len()..]), + _ => { if ! self.target_reached() { self.targets.push(string); } else { self.runtime.push(string); } }, + } + } + + if ! self.target_reached() { utils::print_help_msg("Targets not specified. "); } + } + + fn append_switch(&mut self, arg: impl Into) { self.switch = self.switch.clone()+&arg.into(); } + fn target_reached(&self) -> bool { self.targets.len() == self.target } + pub fn get_runtime(&self) -> &Vec { &self.runtime } + pub fn get_switch(&self) -> &String { &self.switch } + pub fn get_prefix(&self) -> &String { &self.prefix } + pub fn get_targets(&self) -> &Vec { &self.targets } +}