You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

486 lines
14 KiB

//! Searches paths for the plugin name to find it's shared object file
use super::*;
use std::path::{
PathBuf,
Path,
};
use std::{
fs, io,
//collections::HashMap,
};
use regex::{
Regex, RegexBuilder,
};
use lazy_static::lazy_static;
/// Specifies locations and associated rules of these locations for path lookups when searching for plugins.
pub mod locations {
use super::*;
use bitflags::*;
/// Describes a path an its trust level.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
pub enum PathTrust<'a>
{
Trusted(&'a str),
Untrusted(&'a str),
}
bitflags! {
/// The rules a file in an unstructed lookup path must qualify for it to be considered for plugin candidacy.
pub struct UntrustedPathRules : u8
{
/// No restrictions on the file.
///
/// # Note
/// Must always be 0.
const UNRESTRICTED = 0;
/// Name ends in valid string from `LOOKUP_NAME_MATCHES_IN_UNTRUSTED`.
const NAME_MATCH = 1 << 0;
/// Disallow symlinked files.
const NON_SYMLINK = 1 << 1;
/// Disqualify executable-marked files.
const NON_EXEC = 1 << 2;
/// Disqualify world-writable files.
const NON_WORLD_WRITABLE = 1 << 3;
/// Only qualify root-owned files (can be unioned with other `_OWNED_ONLY` constants)
const ROOT_OWNED_ONLY = 1 << 4;
/// Only qualify user-owned files (can be unioned with other `_OWNED_ONLY` constants)
const USER_OWNED_ONLY = 1 << 5;
/// Disqualify files that are accessible at all by anyone other than the current user (or root, which can accesss everything.)
const USER_ACCESSIBLE_ONLY = 1 << 6;
/// Disallow *all* untrusted files.
///
/// # Note
/// Must be highest value.
const INACCESSIBLE = 1 << 7;
}
}
impl UntrustedPathRules
{
/// Contains function pointers for checking paths with every combination (**any**, not **all** in the mask) of rules in `UntrustedPathRules`.
///
/// Invalid functions will either:
/// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.)
/// * Return false immediately.
pub const RULE_FUNCTION_CHECK_TABLE: [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256] = Self::generate_rule_table();
/// Generate a table of 256 entries that which each single, or **OR**-combined mask entry of `UntrustedPathRules` has a corresponding path check function.
///
/// Invalid functions will either:
/// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.)
/// * Return false immediately.
pub(super) const fn generate_rule_table() -> [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256]
{
let mut rule_table = Self::generate_pruned_rule_table();
//TODO: Add in combinations via ORing its single-bit entry functions.
rule_table
}
/// Generate a table of 256 entries that which each **single** (not combinations of) entry of `UntrustedPathRules` has a corresponding path check function.
#[inline]
const fn generate_pruned_rule_table() -> [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256]
{
type RuleCheckFunc<'a> = (dyn Fn(&Path, &str) -> bool + 'a);
#[inline]
const fn disallow(_: &Path, _: &str) -> bool { false }
let mut table: [&'static RuleCheckFunc; 256] = [&(disallow as fn(&Path, &str) -> bool); 256];
macro_rules! set {
($func:expr) => {
{
let func: &'static RuleCheckFunc<'static> = &($func);
func
}
};
($name:ident => $func:expr) => {
let bits = Self::$name.bits as usize;
// Fill in compositions based on previous entries into `table`.
let func: &'static RuleCheckFunc<'static> = set!($func);
/*let rv = {
let mut i: usize = 1;
//XXX: WHY doesn't this work???
while i < bits {
let prev = table[i];
table[bits | i] = set!(|path: &Path, name: &str| -> bool {
prev(path, name) && func(path, name)
});
i += 1;
}*/
table[bits] = func
//};
//rv
};
($name:ident as ! $panic_msg:literal) => {
#[inline(never)]
#[cold]
fn _panic_with_msg() -> ! {
panic!($panic_msg)
}
set!($name => |_, _| _panic_with_msg())
};
($name:ident: $($func_body:tt)+) => {
set!($name => |path, filename| { $($func_body)+})
}
}
set!(NAME_MATCH as ! "This should already have been checked");
set!(NON_SYMLINK => |path: &Path, _| !path.is_symlink());
set!(NON_EXEC => |path, _| {
use std::os::unix::fs::PermissionsExt as _;
use readable_perms::{
User,
Bit,
};
const EXEC_BIT: u32 =
User::Group.mode(Bit::Execute)
| User::Other.mode(Bit::Execute)
| User::Owner.mode(Bit::Execute);
path.metadata()
.map(|meta| meta.permissions().mode() & EXEC_BIT == 0) //TODO: XXX: Test this.
.unwrap_or(false)
});
set!(NON_WORLD_WRITABLE => |path, _| {
use readable_perms::{
PermissionsExt as _,
User,
Bit,
};
path.metadata()
.map(|meta| !meta.permissions().unix().has_mask(User::Other, Bit::Write))
.unwrap_or(false)
});
set!(ROOT_OWNED_ONLY => |path, _| {
use std::os::unix::fs::MetadataExt as _;
path.metadata()
.map(|meta| meta.uid() == 0)
.unwrap_or(false)
});
set!(USER_OWNED_ONLY => |path, _| {
use std::os::unix::fs::MetadataExt as _;
extern "C" {
fn getuid() -> u32; // uid_t
//XXX: Should we use process's UID or EUID? It seems we should use EUID.
fn geteuid() -> u32; // uid_t
}
// SAFETY: This is a pure function for all that matters.
let user_id = unsafe { geteuid() };
path.metadata()
.map(move |meta| meta.uid() == user_id)
.unwrap_or(false)
});
set!(USER_ACCESSIBLE_ONLY => |path, _| {
//Only Owner should be able to read, write, or execute.
use std::os::unix::fs::PermissionsExt as _;
use readable_perms::{
User,
Bit,
};
const BIT_ANY: Bit =
Bit::Read
.union(Bit::Write)
.union(Bit::Execute);
const OTHER_ACCESSORS: u32 =
User::Group.mode(BIT_ANY)
| User::Other.mode(BIT_ANY);
path.metadata()
.map(|meta| !meta.permissions().mode() & OTHER_ACCESSORS == 0) //XXX: Test this
.unwrap_or(false)
});
//TODO: Composition of functions should be done via OR, how to insert all possible compositions into table? We get confusing error messages when we try inside `set!()`
table
}
}
impl Default for UntrustedPathRules
{
#[inline]
fn default() -> Self
{
Self::NAME_MATCH
}
}
impl<'a> AsRef<str> for PathTrust<'a>
{
#[inline(always)]
fn as_ref(&self) -> &str
{
match self {
Trusted(a) | Untrusted(a) => a
}
}
}
impl<'a> From<PathTrust<'a>> for &'a str
{
#[inline]
fn from(from: PathTrust<'a>) -> Self
{
from.into_str()
}
}
impl<'a> From<PathTrust<'a>> for (&'a str, bool)
{
#[inline]
fn from(from: PathTrust<'a>) -> Self
{
let trusted = from.is_trusted();
(from.into_str(), trusted)
}
}
// impl<'a> AsRef<Path> for PathTrust<'a>
// {
// #[inline]
// fn as_ref(&self) -> &Path
// {
// Path::new(AsRef::<str>::as_ref(self))
// }
// }
use PathTrust::*;
// The ordering of these variables is the order in which the plugin is looked-up in. The first match found is used.
/// Should paths be recursively travelled in lookups. (0 is for trusted paths, 1 is for untrusted)
pub static LOOKUP_PATH_RECURSION: (bool, bool) = (true, true);
/// Should symlinks be followed in a recursive lookup. (0 is for trusted paths, 1 is for untrusted)
pub static LOOKUP_PATH_FOLLOW_SYMLINKS: (bool, bool) = (true, false);
/// Trust plugin file lookups in non-contained `LOOKUP_PATH_HOME_BASE_NAME`s.
pub static LOOKUP_PATH_TRUST_HOME: bool = false;
/// Base paths from user home to look-up plugin name.
pub static LOOKUP_PATH_HOME_BASE_NAME: &[PathTrust] = &[
Trusted(".rngcli/plugins/"),
Untrusted(".rngcli/").map_trust(LOOKUP_PATH_TRUST_HOME),
Untrusted(".config/rngcli/").map_trust(LOOKUP_PATH_TRUST_HOME),
Untrusted(".config/").map_trust(LOOKUP_PATH_TRUST_HOME),
];
/// Extra valid lookup paths
pub static LOOKUP_PATH_EXTRA_PATHS: &[PathTrust] = &[
Trusted("/usr/share/rngcli/"),
Trusted("/usr/local/share/rngcli/")
];
/// `PATH`-like env-vars to look up a plugin's name.
pub static LOOKUP_PATH_ENV_VARS: &[PathTrust] = &[
Trusted("RNGCLI_PATH"),
Untrusted("PATH"),
];
/// Allow lookup inside the current working directory.
pub static LOOKUP_PATH_ALLOW_CWD: bool = true;
/// Files with these regex-matched names are considered in all lookup contexts (`Trusted` and `Untrusted`.)
pub static LOOKUP_NAME_MATCHES_IN_TRUSTED: &[&'static str] = &[
r"\.rngp\.so$",
r"\.rngp$",
r"\.so$",
];
/// Files with these regex-matched names are considered in `Untrusted` lookup contexts (if specified by `LOOKUP_UNTRUSTED_PATH_RULES`)
pub static LOOKUP_NAME_MATCHES_IN_UNTRUSTED: &[&'static str] = &[
r"\.rngp\.so$",
r"\.rngp$",
];
/// These rules must be met for lookups in untrusted paths.
pub static LOOKUP_UNTRUSTED_PATH_RULES: UntrustedPathRules =
UntrustedPathRules::NAME_MATCH
.union(UntrustedPathRules::NON_SYMLINK)
.union(UntrustedPathRules::NON_WORLD_WRITABLE);
impl<'a> PathTrust<'a>
{
#[inline(always)]
pub const fn into_str(self) -> &'a str
{
match self {
Trusted(a) | Untrusted(a) => a
}
}
#[inline(always)]
pub fn into_path(self) -> &'a Path
{
match self {
Trusted(a) | Untrusted(a) => Path::new(a)
}
}
#[inline(always)]
pub fn as_path(&self) -> &Path
{
Path::new(self.as_ref())
}
#[inline(always)]
pub const fn is_trusted(self) -> bool
{
if let Trusted(_) = self {
true
} else {
false
}
}
#[inline(always)]
pub const fn is_untrusted(self) -> bool
{
!self.is_trusted()
}
/// Map the trust level of this `PathTrust`.
#[inline(always)]
pub const fn map_trust(self, trust: bool) -> Self
{
if trust { Trusted(self.into_str()) }
else { Untrusted(self.into_str()) }
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct LookupVisitor<P: AsRef<Path>>(P);
#[derive(Debug)]
struct PathLookup {
name: String,
candidates: Vec<fs::DirEntry>,
trusted: bool,
}
impl PathLookup
{
#[inline]
pub fn check_valid_path<const TRUSTED: bool>(path: impl AsRef<Path>) -> bool
{
#[inline(always)]
fn regex_builder(pattern: impl AsRef<str>) -> Regex {
RegexBuilder::new(pattern.as_ref())
.case_insensitive(true)
.build().expect("Invalid regex in trusted pattern match array.")
}
/// Returns a function that returns `true` if any of the `matchers` match the input string.
#[inline]
const fn match_any<'a, I: 'a>(matchers: I) -> impl Fn(&str) -> bool + 'a
where I: IntoIterator<Item = &'a Regex> + Clone,
{
move |s: &str| -> bool {
let matchers = matchers.clone().into_iter();
for re in matchers {
if re.is_match(s) {
return true;
}
}
false
}
}
/// Validate a file in an untrusted lookup.
#[inline]
fn validate_file_untrusted(path: &Path, filename: &str) -> bool
{
// By this point, `filename` has already matched successfully.
// Also Note: `filename` is just the basename of `path` as an `&str` (aliasing address-space.)
use locations::UntrustedPathRules as Rule;
use locations::LOOKUP_UNTRUSTED_PATH_RULES as RULES;
let mut bit = Rule::INACCESSIBLE.bits();
// No rules
if bit == 0 /* `Rule::UNRESTRICTED` */ {
return true;
}
while bit > 0
{
// SAFETY: We know `INACCESSIBLE` is the largest
if RULES.contains(unsafe { Rule::from_bits_unchecked(bit) }) {
}
bit >>= 1;
}
todo!("TODO: extra Untrusted lookup rules checked on `path` here.")
}
lazy_static! {
static ref TRUSTED_EXT_REGEX_MAP: Vec<Regex> =
locations::LOOKUP_NAME_MATCHES_IN_TRUSTED.into_iter().map(regex_builder).collect();
static ref UNTRUSTED_EXT_REGEX_MAP: Vec<Regex> =
locations::LOOKUP_NAME_MATCHES_IN_UNTRUSTED.into_iter().map(regex_builder).collect();
}
let path = path.as_ref();
// Check if the path:
// * Exists
// * Is not a directory
// NOTE: We don't use `.is_file()` here, because we want to support symlinks and pseudofiles in trusted contexts, which `is_file()` reports as false.
// TODO: XXX: Maybe in the future allow directories to be plugins; they would contain a manifest, and each `.so` file in the directory will be loaded in order specified by the directory's manifest.
if !path.exists() || path.is_dir() {
return false;
}
// Attempt to extract the filename, return false if it is invalid UTF8.
let filename = match path.file_name()
.and_then(|os_fn| os_fn.to_str())
{
Some(f) => f,
_ => return false,
};
// Get the name matcher and rule checker for the file.
let (matcher, checker) = if TRUSTED {
// Trusted path lookup
(match_any(TRUSTED_EXT_REGEX_MAP.iter()), None)//&NoChecker)
} else {
// Untrusted path lookup
let matcher = if locations::LOOKUP_UNTRUSTED_PATH_RULES
.contains(locations::UntrustedPathRules::NAME_MATCH)
{
// Name must match
match_any(UNTRUSTED_EXT_REGEX_MAP.iter())
} else {
// Name needn't match
match_any(TRUSTED_EXT_REGEX_MAP.iter())
};
(matcher, Some(move |path| {
validate_file_untrusted(path, filename)
}))
};
matcher(filename) // Check filename first
&& match checker {
Some(untrusted) => untrusted(path), // Check extra Untrusted path rules next
_ => true
}
}
}
impl<P: AsRef<Path>> LookupVisitor<P>
{
pub fn start_lookup(&self, trusted: bool, name: impl Into<String>) -> io::Result<PathLookup>
{
Ok(PathLookup {
name: name.into(),
trusted,
candidates: fs::read_dir(&self.0)?.collect::<io::Result<Vec<_>>>()?,
})
}
}
pub fn lookup_plugin_name(name: impl AsRef<str>) -> Option<PathBuf>
{
todo!("Create a LookupVisitor(<path>) for each path in `locations`, then call `start_lookup(<trusted path>, name)` on each *in order* specified in `locations`")
}