mod ffi;
mod util;
extern crate libc;
extern crate nix;
use ffi::{GroupRecord, PasswdRecord};
use nix::fcntl::{open, OFlag};
use nix::sys::stat::{umask, Mode};
#[cfg(not(target_os = "macos"))]
use nix::unistd::{
chdir, chown, close, dup2, fork, getpid, initgroups, setgid, setsid, setuid, ForkResult, Gid,
Pid, Uid,
};
#[cfg(target_os = "macos")]
use nix::unistd::{
chdir, chown, close, dup2, fork, getpid, setgid, setsid, setuid, ForkResult, Gid, Pid, Uid,
};
use std::convert::TryFrom;
use std::error::Error;
use std::ffi::CString;
use std::fmt;
use std::fmt::{Debug, Display};
use std::fs::File;
use std::io::prelude::*;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::process::exit;
#[derive(Debug, PartialOrd, PartialEq, Clone)]
pub enum DaemonError {
Fork,
ChDir,
OpenDevNull,
CloseFp,
InvalidUser,
InvalidGroup,
InvalidUserGroupPair,
InitGroups,
SetUid,
SetGid,
ChownPid,
OpenPid,
WritePid,
RedirectStream,
InvalidUmaskBits,
SetSid,
#[doc(hidden)]
__Nonexhaustive,
}
impl DaemonError {
fn __description(&self) -> &str {
match *self {
DaemonError::Fork => "Unable to fork",
DaemonError::ChDir => "Failed to chdir",
DaemonError::OpenDevNull => "Failed to open dev null",
DaemonError::CloseFp => "Failed to close the file pointer of a stdio stream",
DaemonError::InvalidUser => "Invalid or nonexistent user",
DaemonError::InvalidGroup => "Invalid or nonexistent group",
DaemonError::InvalidUserGroupPair => {
"Either group or user was specified but no the other"
}
DaemonError::InitGroups => "Failed to execute initgroups",
DaemonError::SetUid => "Failed to set uid",
DaemonError::SetGid => "Failed to set gid",
DaemonError::ChownPid => "Failed to chown the pid file",
DaemonError::OpenPid => "Failed to create the pid file",
DaemonError::WritePid => "Failed to write to the pid file",
DaemonError::RedirectStream => "Failed to redirect the standard streams",
DaemonError::InvalidUmaskBits => "Umask bits are invalid",
DaemonError::SetSid => "Failed to set sid",
DaemonError::__Nonexhaustive => unreachable!(),
}
}
}
impl Display for DaemonError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
std::fmt::Debug::fmt(&self.__description(), f)
}
}
impl Error for DaemonError {}
pub type Result<T> = std::result::Result<T, DaemonError>;
#[derive(Debug, Ord, PartialOrd, PartialEq, Eq, Clone)]
pub enum User {
Id(u32),
}
impl<'uname> TryFrom<&'uname str> for User {
type Error = DaemonError;
fn try_from(uname: &'uname str) -> Result<User> {
match PasswdRecord::get_record_by_name(uname) {
Ok(record) => Ok(User::Id(record.pw_uid)),
Err(_) => Err(DaemonError::InvalidUser),
}
}
}
impl TryFrom<String> for User {
type Error = DaemonError;
fn try_from(uname: String) -> Result<User> {
match PasswdRecord::get_record_by_name(uname.as_str()) {
Ok(record) => Ok(User::Id(record.pw_uid)),
Err(_) => Err(DaemonError::InvalidUser),
}
}
}
impl From<u32> for User {
fn from(uid: u32) -> User {
User::Id(uid)
}
}
#[derive(Debug, Ord, PartialOrd, PartialEq, Eq, Clone)]
pub enum Group {
Id(u32),
}
impl<'uname> TryFrom<&'uname str> for Group {
type Error = DaemonError;
fn try_from(gname: &'uname str) -> Result<Group> {
match GroupRecord::get_record_by_name(gname) {
Ok(record) => Ok(Group::Id(record.gr_gid)),
Err(_) => Err(DaemonError::InvalidGroup),
}
}
}
impl TryFrom<String> for Group {
type Error = DaemonError;
fn try_from(gname: String) -> Result<Group> {
match GroupRecord::get_record_by_name(gname.as_str()) {
Ok(record) => Ok(Group::Id(record.gr_gid)),
Err(_) => Err(DaemonError::InvalidGroup),
}
}
}
impl From<u32> for Group {
fn from(uid: u32) -> Group {
Group::Id(uid)
}
}
#[derive(Debug)]
enum StdioImp {
Devnull,
RedirectToFile(File),
}
#[derive(Debug)]
pub struct Stdio {
inner: StdioImp,
}
impl Stdio {
fn devnull() -> Self {
Self {
inner: StdioImp::Devnull,
}
}
}
impl From<File> for Stdio {
fn from(file: File) -> Self {
Self {
inner: StdioImp::RedirectToFile(file),
}
}
}
pub struct Daemon {
chdir: PathBuf,
pid_file: Option<PathBuf>,
chown_pid_file: bool,
user: Option<User>,
group: Option<Group>,
umask: u16,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
}
fn redirect_stdio(stdin: &Stdio, stdout: &Stdio, stderr: &Stdio) -> Result<()> {
let devnull_fd = match open(
Path::new("/dev/null"),
OFlag::O_APPEND,
Mode::from_bits(OFlag::O_RDWR.bits() as _).unwrap(),
) {
Ok(fd) => fd,
Err(_) => return Err(DaemonError::OpenDevNull),
};
let proc_stream = |fd, stdio: &Stdio| {
match close(fd) {
Ok(_) => (),
Err(_) => return Err(DaemonError::CloseFp),
};
return match &stdio.inner {
StdioImp::Devnull => match dup2(devnull_fd, fd) {
Ok(_) => Ok(()),
Err(_) => Err(DaemonError::RedirectStream),
},
StdioImp::RedirectToFile(file) => {
let raw_fd = file.as_raw_fd();
match dup2(raw_fd, fd) {
Ok(_) => Ok(()),
Err(_) => Err(DaemonError::RedirectStream),
}
}
};
};
proc_stream(libc::STDIN_FILENO, stdin)?;
proc_stream(libc::STDOUT_FILENO, stdout)?;
proc_stream(libc::STDERR_FILENO, stderr)?;
Ok(())
}
impl Daemon {
pub fn new() -> Self {
Daemon {
chdir: Path::new("/").to_owned(),
pid_file: None,
chown_pid_file: false,
user: None,
group: None,
umask: 0o027,
stdin: Stdio::devnull(),
stdout: Stdio::devnull(),
stderr: Stdio::devnull(),
}
}
pub fn pid_file<T: AsRef<Path>>(mut self, path: T, chmod: Option<bool>) -> Self {
self.pid_file = Some(path.as_ref().to_owned());
self.chown_pid_file = chmod.unwrap_or(false);
self
}
pub fn work_dir<T: AsRef<Path>>(mut self, path: T) -> Self {
self.chdir = path.as_ref().to_owned();
self
}
pub fn user<T: Into<User>>(mut self, user: T) -> Self {
self.user = Some(user.into());
self
}
pub fn group<T: Into<Group>>(mut self, group: T) -> Self {
self.group = Some(group.into());
self
}
pub fn umask(mut self, mask: u16) -> Self {
self.umask = mask;
self
}
pub fn stdin<T: Into<Stdio>>(mut self, stdio: T) -> Self {
self.stdin = stdio.into();
self
}
pub fn stdout<T: Into<Stdio>>(mut self, stdio: T) -> Self {
self.stdout = stdio.into();
self
}
pub fn stderr<T: Into<Stdio>>(mut self, stdio: T) -> Self {
self.stderr = stdio.into();
self
}
pub fn start(self) -> Result<()> {
let pid: Pid;
let has_pid_file = self.pid_file.is_some();
let pid_file_path = match self.pid_file {
Some(path) => path.clone(),
None => Path::new("").to_path_buf(),
};
redirect_stdio(&self.stdin, &self.stdout, &self.stderr)?;
if self.chown_pid_file && (self.user.is_none() || self.group.is_none()) {
return Err(DaemonError::InvalidUserGroupPair);
} else if (self.user.is_some() || self.group.is_some())
&& (self.user.is_none() || self.group.is_none())
{
return Err(DaemonError::InvalidUserGroupPair);
}
match fork() {
Ok(ForkResult::Parent { child: _ }) => exit(0),
Ok(ForkResult::Child) => (),
Err(_) => return Err(DaemonError::Fork),
}
let umask_mode = match Mode::from_bits(self.umask as _) {
Some(mode) => mode,
None => return Err(DaemonError::InvalidUmaskBits),
};
umask(umask_mode);
if let Err(_) = setsid() {
return Err(DaemonError::SetSid);
};
if let Err(_) = chdir::<Path>(self.chdir.as_path()) {
return Err(DaemonError::ChDir);
};
pid = getpid();
if has_pid_file {
let pid_file = &pid_file_path;
match File::create(pid_file) {
Ok(mut fp) => {
if let Err(_) = fp.write_all(pid.to_string().as_ref()) {
return Err(DaemonError::WritePid);
}
}
Err(_) => return Err(DaemonError::WritePid),
};
}
if self.user.is_some() && self.group.is_some() {
let user = match self.user.unwrap() {
User::Id(id) => Uid::from_raw(id),
};
let uname = match PasswdRecord::get_record_by_id(user.as_raw()) {
Ok(record) => record.pw_name,
Err(_) => return Err(DaemonError::InvalidUser),
};
let gr = match self.group.unwrap() {
Group::Id(id) => Gid::from_raw(id),
};
if self.chown_pid_file && has_pid_file {
match chown(&pid_file_path, Some(user), Some(gr)) {
Ok(_) => (),
Err(_) => return Err(DaemonError::ChownPid),
};
}
match setgid(gr) {
Ok(_) => (),
Err(_) => return Err(DaemonError::SetGid),
};
#[cfg(not(target_os = "macos"))]
{
let u_cstr = match CString::new(uname) {
Ok(cstr) => cstr,
Err(_) => return Err(DaemonError::SetGid),
};
match initgroups(&u_cstr, gr) {
Ok(_) => (),
Err(_) => return Err(DaemonError::InitGroups),
};
}
match setuid(user) {
Ok(_) => (),
Err(_) => return Err(DaemonError::SetUid),
}
}
let chdir_path = self.chdir.to_owned();
match chdir::<Path>(chdir_path.as_ref()) {
Ok(_) => (),
Err(_) => return Err(DaemonError::ChDir),
};
Ok(())
}
}
#[cfg(test)]
mod tests {
extern crate nix;
use super::*;
#[test]
fn test_uname_to_uid_resolution() {
let daemon = Daemon::new().user(User::try_from("root").unwrap());
assert!(daemon.user.is_some());
let uid = match daemon.user.unwrap() {
User::Id(id) => id,
};
assert_eq!(uid, 0)
}
}