From ef5fdc82a0f504ef2cccad045c7cce7df7de5d8f Mon Sep 17 00:00:00 2001 From: Jose Narvaez Date: Mon, 17 Jul 2017 18:20:58 +0100 Subject: [PATCH] Improved `id` and `whoami`. Details - New implementation of `id` inspired by BSD's one. - Refactoring on `whoami`. - Added few utility functions in `userutils` for getting processes user and group real/effective ID and names. Added docs do `getty`, `login`, `passwd`, `su`, and `sudo`. Details - The docs are from BSD's ones. - Also added `ArgParser` to all of them and added a -h/--help flag to all of them. - This is the start of a revision of all of those commands. --- src/bin/getty.rs | 37 ++++++++ src/bin/id.rs | 218 +++++++++++++++++++++++++++++++++++++++++++++- src/bin/login.rs | 36 +++++++- src/bin/passwd.rs | 40 ++++++++- src/bin/su.rs | 38 +++++++- src/bin/sudo.rs | 41 +++++++++ src/bin/whoami.rs | 38 ++++---- src/lib.rs | 198 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 617 insertions(+), 29 deletions(-) diff --git a/src/bin/getty.rs b/src/bin/getty.rs index 581d166c8a..1f5007e89c 100644 --- a/src/bin/getty.rs +++ b/src/bin/getty.rs @@ -1,11 +1,35 @@ #![deny(warnings)] extern crate syscall; +extern crate arg_parser; use std::process::Command; use std::{env, process, str}; use std::io::{self, Write}; +use arg_parser::ArgParser; + +const MAN_PAGE: &'static str = /* @MANSTART{login} */ r#" +NAME + getty - set terminal mode + +SYNOPSIS + getty + +DESCRIPTION + The getty utility is called by init(8) to open and initialize the tty line, + read a login name, and invoke login(1). + +OPTIONS + + -h + --help + Display this help and exit. + +AUTHOR + Written by Jeremy Soller. +"#; + fn set_tty(tty: &str) -> syscall::Result<()> { let stdin = syscall::open(tty, syscall::flag::O_RDONLY)?; let stdout = syscall::open(tty, syscall::flag::O_WRONLY)?; @@ -39,6 +63,19 @@ fn daemon(clear: bool) { } pub fn main() { + let mut stdout = io::stdout(); + + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + let _ = stdout.write_all(MAN_PAGE.as_bytes()); + let _ = stdout.flush(); + process::exit(0); + } + let mut tty_option = None; let mut clear = true; for arg in env::args().skip(1) { diff --git a/src/bin/id.rs b/src/bin/id.rs index b161353678..54487fa05f 100644 --- a/src/bin/id.rs +++ b/src/bin/id.rs @@ -1,9 +1,221 @@ +extern crate arg_parser; +extern crate extra; extern crate syscall; +extern crate userutils; +use std::borrow::Borrow; +use std::hash::Hash; use std::env; +use std::io::{self, Write, Stderr, StdoutLock}; +use std::process::exit; + +use extra::io::fail; +use extra::option::OptionalExt; +use arg_parser::{ArgParser, Param}; +use userutils::{get_egid, get_gid, get_euid, get_uid, get_user, get_group}; + +const HELP_INFO: &'static str = "Try ‘id --help’ for more information.\n"; +const MAN_PAGE: &'static str = /* @MANSTART{id} */ r#" +NAME + id - display user identity + +SYNOPSIS + id + id -g [-nr] + id -u [-nr] + id [ -h | --help ] + +DESCRIPTION + The id utility displays the user and group names and numeric IDs, + of the calling process, to the standard output. + +OPTIONS + -G + Display the different group IDs (effective and real) + as white-space separated numbers, in no particular order. + + -g + Display the effective group ID as a number. + + -n Display the name of the user or group ID for the -g and -u + options instead of the number. + + -u + Display the effective user ID as a number. + + -a + Ignored for compatibility with other id implementations. + + -h + --help + Display this help and exit. + +EXIT STATUS + The whoami utility exits 0 on success, and >0 if an error occurs. + +AUTHOR + Written by Jose Narvaez. +"#; /* @MANEND */ pub fn main() { - let uid = syscall::getuid().expect("id: failed to get UID"); - let gid = syscall::getgid().expect("id: failed to get GID"); - println!("uid={}({}) gid={}({})", uid, env::var("USER").unwrap_or(String::new()), gid, ""); + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + let mut stderr = io::stderr(); + + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]) + .add_flag(&["a"]) + .add_flag(&["G"]) + .add_flag(&["g"]) + .add_flag(&["u"]) + .add_flag(&["n"]) + .add_flag(&["r"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + print_msg(MAN_PAGE, &mut stdout, &mut stderr); + exit(0); + } + + // Unrecognized flags + if let Err(err) = parser.found_invalid() { + stderr.write_all(err.as_bytes()).try(&mut stderr); + print_msg(HELP_INFO, &mut stdout, &mut stderr); + exit(1); + } + + // Display the different group IDs (effective and real) + // as white-space separated numbers, in no particular order. + if parser.found(&'G') { + if any_of_found(&parser, &[&'g', &'u']) { + let msg = "id: -G option must be used without others\n"; + print_msg(msg, &mut stdout, &mut stderr); + print_msg(HELP_INFO, &mut stdout, &mut stderr); + exit(1); + } + + let egid = get_egid(&mut stderr); + let gid = get_gid(&mut stderr); + print_msg(&format!("{} {}\n", egid, gid), &mut stdout, &mut stderr); + exit(0); + } + + // Check if people passed both -g -u which are mutually exclusive + if parser.found(&'u') && parser.found(&'g') { + let msg = "id: specify either -u or -g but not both\n"; + print_msg(msg, &mut stdout, &mut stderr); + print_msg(HELP_INFO, &mut stdout, &mut stderr); + exit(1); + } + + // Display effective/real process user ID UNIX user name + if parser.found(&'u') && parser.found(&'n') { + // Did they pass -r? F so, we show the real + let uid = if parser.found(&'r') { + get_uid(&mut stderr) + } else { + get_euid(&mut stderr) + }; + + get_user(uid, &mut stderr).map(|user| { + print_msg(&format!("{}\n", user), &mut stdout, &mut stderr); + exit(0); + }).or_else(|| { + fail(&format!("id: no user found for uid: {}", uid), &mut stderr) + }); + } + + // Display real user ID + if parser.found(&'u') && parser.found(&'r') { + let uid = get_uid(&mut stderr); + print_msg(&format!("{}\n", uid), &mut stdout, &mut stderr); + exit(0); + } + + // Display effective user ID + if parser.found(&'u') { + let euid = get_euid(&mut stderr); + print_msg(&format!("{}\n", euid), &mut stdout, &mut stderr); + exit(0); + } + + // Display effective/real process group ID UNIX group name + if parser.found(&'g') && parser.found(&'n') { + // Did they pass -r? If so we show the real one + let gid = if parser.found(&'r') { + get_gid(&mut stderr) + } else { + get_egid(&mut stderr) + }; + + get_group(gid, &mut stderr).map(|group| { + print_msg(&format!("{}\n", group), &mut stdout, &mut stderr); + exit(0); + }).or_else(|| { + fail(&format!("id: no group found for gid: {}", gid), &mut stderr) + }); + } + + // Display the real group ID + if parser.found(&'g') && parser.found(&'r') { + let gid = get_gid(&mut stderr); + print_msg(&format!("{}\n", gid), &mut stdout, &mut stderr); + exit(0); + } + + // Display effective group ID + if parser.found(&'g') { + let egid = get_egid(&mut stderr); + print_msg(&format!("{}\n", egid), &mut stdout, &mut stderr); + exit(0); + } + + // -n does not apply if there is no -u or -g + if parser.found(&'n') && none_of_found(&parser, &[&'u', &'g']) { + let msg = "id: the -n option must be used with either -u or -g\n"; + fail(msg, &mut stderr); + } + + // -r does not apply if there is no -u or -g + if parser.found(&'r') && none_of_found(&parser, &[&'u', &'g']) { + let msg = "id: the -r option must be used with either -u or -g\n"; + fail(msg, &mut stderr); + } + + // We get everything we can and show that + let euid = get_euid(&mut stderr); + let egid = get_egid(&mut stderr); + let user = get_user(euid, &mut stderr).unwrap_or_else(|| { + fail(&format!("id: no user found for uid: {}", euid), &mut stderr); + }); + + let group = get_group(egid, &mut stderr).unwrap_or_else(|| { + fail(&format!("id: no group found for gid: {}", euid), &mut stderr); + }); + + let msg = format!("uid={}({}) gid={}({})\n", euid, user, egid, group); + print_msg(&msg, &mut stdout, &mut stderr); + exit(0); +} + +pub fn any_of_found(parser: &ArgParser, flags: &[&P]) -> bool + where Param: Borrow

+{ + for flag in flags { + if parser.found(*flag) { return true } + } + + false +} + +fn none_of_found(parser: &ArgParser, flags: &[&P]) -> bool + where Param: Borrow

+{ + !any_of_found(parser, flags) +} + +fn print_msg(msg: &str, stdout: &mut StdoutLock, stderr: &mut Stderr) { + stdout.write_all(msg.as_bytes()).try(stderr); + stdout.flush().try(stderr); } diff --git a/src/bin/login.rs b/src/bin/login.rs index 983a9ff842..6245d45920 100644 --- a/src/bin/login.rs +++ b/src/bin/login.rs @@ -3,19 +3,53 @@ extern crate liner; extern crate termion; extern crate userutils; +extern crate arg_parser; use std::fs::File; use std::io::{self, Read, Write}; use std::os::unix::process::CommandExt; -use std::process::Command; +use std::process::{self, Command}; +use std::env; use std::str; +use arg_parser::ArgParser; use termion::input::TermRead; use userutils::Passwd; +const MAN_PAGE: &'static str = /* @MANSTART{login} */ r#" +NAME + login - log into the computer + +SYNOPSIS + login + +DESCRIPTION + The login utility logs users (and pseudo-users) into the computer system. + +OPTIONS + + -h + --help + Display this help and exit. + +AUTHOR + Written by Jeremy Soller. +"#; + pub fn main() { let mut stdout = io::stdout(); + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + let _ = stdout.write_all(MAN_PAGE.as_bytes()); + let _ = stdout.flush(); + process::exit(0); + } + if let Ok(mut issue) = File::open("/etc/issue") { io::copy(&mut issue, &mut stdout).unwrap(); let _ = stdout.flush(); diff --git a/src/bin/passwd.rs b/src/bin/passwd.rs index ed88511183..18c7a03b65 100644 --- a/src/bin/passwd.rs +++ b/src/bin/passwd.rs @@ -1,3 +1,4 @@ +extern crate arg_parser; extern crate rand; extern crate syscall; extern crate termion; @@ -7,15 +8,52 @@ use rand::{Rng, OsRng}; use std::{env, io}; use std::fs::File; use std::io::{Read, Write}; +use std::process; + +use arg_parser::ArgParser; use termion::input::TermRead; use userutils::Passwd; +const MAN_PAGE: &'static str = /* @MANSTART{passwd} */ r#" +NAME + passwd - modify a user's password + +SYNOPSIS + passwd + passwd [ -h | --help ] + +DESCRIPTION + The passwd utility changes the user's local password. If the user is not + the super-user, passwd first prompts for the current password and will + not continue unless the correct password is entered. + +OPTIONS + + -h + --help + Display this help and exit. + +AUTHOR + Written by Jeremy Soller. +"#; + fn main() { let stdin = io::stdin(); let mut stdin = stdin.lock(); let stdout = io::stdout(); let mut stdout = stdout.lock(); + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + let _ = stdout.write_all(MAN_PAGE.as_bytes()); + let _ = stdout.flush(); + process::exit(0); + } + let uid = syscall::getuid().unwrap() as u32; let mut passwd_string = String::new(); @@ -87,7 +125,7 @@ fn main() { if let Some(confirm_password) = stdin.read_passwd(&mut stdout).unwrap() { stdout.write(b"\n").unwrap(); let _ = stdout.flush(); - + if new_password == confirm_password { let salt = format!("{:X}", OsRng::new().unwrap().next_u64()); writeln!(stdout, "{}", userutils::Passwd::encode(&new_password, &salt)).unwrap(); diff --git a/src/bin/su.rs b/src/bin/su.rs index 3b6cd7897d..05a2e56179 100644 --- a/src/bin/su.rs +++ b/src/bin/su.rs @@ -1,23 +1,59 @@ extern crate syscall; extern crate termion; extern crate userutils; +extern crate arg_parser; use std::env; use std::fs::File; use std::io::{self, Read, Write}; use std::os::unix::process::CommandExt; -use std::process::Command; +use std::process::{self, Command}; use std::str; +use arg_parser::ArgParser; use termion::input::TermRead; use userutils::Passwd; +const MAN_PAGE: &'static str = /* @MANSTART{su} */ r#" +NAME + su - substitute user identity + +SYNOPSIS + su + su command + su [ -h | --help ] + +DESCRIPTION + The su utility requests appropriate user credentials via PAM and switches to + that user ID (the default user is the superuser). A shell is then executed. + +OPTIONS + + -h + --help + Display this help and exit. + +AUTHOR + Written by Jeremy Soller. +"#; + pub fn main() { let stdin = io::stdin(); let mut stdin = stdin.lock(); let stdout = io::stdout(); let mut stdout = stdout.lock(); + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + let _ = stdout.write_all(MAN_PAGE.as_bytes()); + let _ = stdout.flush(); + process::exit(0); + } + let mut user = env::args().nth(1).unwrap_or(String::new()); if user.is_empty() { user = String::from("root"); diff --git a/src/bin/sudo.rs b/src/bin/sudo.rs index dbfb98ff86..64ca4498f5 100644 --- a/src/bin/sudo.rs +++ b/src/bin/sudo.rs @@ -1,3 +1,5 @@ +extern crate arg_parser; +extern crate extra; extern crate syscall; extern crate termion; extern crate userutils; @@ -8,9 +10,37 @@ use std::io::{self, Read, Write}; use std::os::unix::process::CommandExt; use std::process::{self, Command}; +use arg_parser::ArgParser; use termion::input::TermRead; use userutils::{Passwd, Group}; +const MAN_PAGE: &'static str = /* @MANSTART{sudo} */ r#" +NAME + sudo - execute a command as another user + +SYNOPSIS + sudo command + sudo [ -h | --help ] + +DESCRIPTION + The sudo utility allows a permitted user to execute a command as the + superuser or another user, as specified by the security policy. + +OPTIONS + + -h + --help + Display this help and exit. + +EXIT STATUS + Upon successful execution of a command, the exit status from sudo will + be the exit status of the program that was executed. In case of error + the exit status will be >0. + +AUTHOR + Written by Jeremy Soller. +"#; + pub fn main() { let stdin = io::stdin(); let mut stdin = stdin.lock(); @@ -19,6 +49,17 @@ pub fn main() { let stderr = io::stderr(); let mut stderr = stderr.lock(); + let mut parser = ArgParser::new(1) + .add_flag(&["h", "help"]); + parser.parse(env::args()); + + // Shows the help + if parser.found("help") { + let _ = stdout.write_all(MAN_PAGE.as_bytes()); + let _ = stdout.flush(); + process::exit(0); + } + let mut args = env::args().skip(1); match args.next() { None => { diff --git a/src/bin/whoami.rs b/src/bin/whoami.rs index 96ed8bb501..9a54bc14d8 100644 --- a/src/bin/whoami.rs +++ b/src/bin/whoami.rs @@ -5,13 +5,12 @@ extern crate extra; extern crate syscall; extern crate userutils; -use std::io::{self, Read, Write}; +use std::io::{self, Write}; use std::process::exit; -use std::fs::File; use std::env; use arg_parser::ArgParser; use extra::option::OptionalExt; -use userutils::Passwd; +use userutils::{get_euid, get_user}; const MAN_PAGE: &'static str = /* @MANSTART{whoami} */ r#" NAME @@ -35,8 +34,6 @@ AUTHOR Written by Jose Narvaez. "#; /* @MANEND */ -const PASSWD_FILE: &'static str = "/etc/passwd"; - fn main() { let stdout = io::stdout(); let mut stdout = stdout.lock(); @@ -52,23 +49,18 @@ fn main() { exit(0); } - match syscall::geteuid() { - Ok(euid) => { - let mut passwd_string = String::new(); - if let Ok(mut file) = File::open(PASSWD_FILE) { - let _ = file.read_to_string(&mut passwd_string); - } - - for line in passwd_string.lines() { - if let Ok(passwd) = Passwd::parse(line) { - if euid == passwd.uid as usize { - stdout.write_all(format!("{}\n", passwd.name).as_bytes()).try(&mut stderr); - stdout.flush().try(&mut stderr); - exit(0); - } - } - } - }, - Err(_) => exit(1) + let euid = get_euid(&mut stderr); + let user = match get_user(euid, &mut stderr) { + Some(user) => user, + None => { + let msg = format!("whoami: no user found for uid: {}", euid); + stdout.write_all(msg.as_bytes()).try(&mut stderr); + stdout.flush().try(&mut stderr); + exit(1); + } }; + + stdout.write_all(format!("{}\n", user).as_bytes()).try(&mut stderr); + stdout.flush().try(&mut stderr); + exit(0); } diff --git a/src/lib.rs b/src/lib.rs index f4c40f7009..856eebe345 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,19 @@ extern crate argon2rs; +extern crate extra; +extern crate syscall; + +use std::io::{Read, Stderr, Write}; +use std::fs::File; +use std::process::exit; use argon2rs::verifier::Encoded; use argon2rs::{Argon2, Variant}; +use extra::option::OptionalExt; +const PASSWD_FILE: &'static str = "/etc/passwd"; +const GROUP_FILE: &'static str = "/etc/group"; + +/// A struct representing a UNIX /etc/passwd file entry pub struct Passwd<'a> { pub user: &'a str, pub hash: &'a str, @@ -36,6 +47,18 @@ impl<'a> Passwd<'a> { }) } + pub fn parse_file(file_data: &'a str) -> Result>, ()> { + let mut entries: Vec> = Vec::new(); + + for line in file_data.lines() { + if let Ok(passwd) = Passwd::parse(line) { + entries.push(passwd); + } + } + + Ok(entries) + } + pub fn encode(password: &str, salt: &str) -> String { let a2 = Argon2::new(10, 1, 4096, Variant::Argon2i).unwrap(); let e = Encoded::new(a2, password.as_bytes(), salt.as_bytes(), &[], &[]); @@ -48,6 +71,7 @@ impl<'a> Passwd<'a> { } } +/// A struct representing a UNIX /etc/group file entry pub struct Group<'a> { pub group: &'a str, pub gid: u32, @@ -68,4 +92,178 @@ impl<'a> Group<'a> { users: users }) } + + pub fn parse_file(file_data: &'a str) -> Result>, ()> { + let mut entries: Vec> = Vec::new(); + + for line in file_data.lines() { + if let Ok(group) = Group::parse(line) { + entries.push(group); + } + } + + Ok(entries) + } +} + +/// Gets the current process effective user id aborting the caller on error. +/// +/// This function issues the `geteuid` system call returning the process effective +/// user id. In case of an error it will log message to `stderr` and then abort +/// the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let euid = get_euid(&mut stderr); +/// +/// ``` +pub fn get_euid(stderr: &mut Stderr) -> usize { + match syscall::geteuid() { + Ok(euid) => euid, + Err(_) => { + let mut stderr = stderr.lock(); + let _ = stderr.write_all(b"failed to get effective UID\n"); + let _ = stderr.flush(); + exit(1) + } + } +} + +/// Gets the current process real user id aborting the caller on error. +/// +/// This function issues the `getuid` system call returning the process real +/// user id. In case of an error it will log message to `stderr` and then abort +/// the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let uid = get_uid(&mut stderr); +/// +/// ``` +pub fn get_uid(stderr: &mut Stderr) -> usize { + match syscall::getuid() { + Ok(euid) => euid, + Err(_) => { + let mut stderr = stderr.lock(); + let _ = stderr.write_all(b"failed to get real UID\n"); + let _ = stderr.flush(); + exit(1) + } + } +} + +/// Gets the current process effective group id aborting the caller on error. +/// +/// This function issues the `getegid` system call returning the process effective +/// group id. In case of an error it will log message to `stderr` and then abort +/// the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let egid = get_egid(&mut stderr); +/// +/// ``` +pub fn get_egid(stderr: &mut Stderr) -> usize { + match syscall::getegid() { + Ok(euid) => euid, + Err(_) => { + let mut stderr = stderr.lock(); + let _ = stderr.write_all(b"failed to get effective GID\n"); + let _ = stderr.flush(); + exit(1) + } + } +} + +/// Gets the current process real group id aborting the caller on error. +/// +/// This function issues the `getegid` system call returning the process real +/// group id. In case of an error it will log message to `stderr` and then abort +/// the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let gid = get_gid(&mut stderr); +/// +/// ``` +pub fn get_gid(stderr: &mut Stderr) -> usize { + match syscall::getgid() { + Ok(euid) => euid, + Err(_) => { + let mut stderr = stderr.lock(); + let _ = stderr.write_all(b"failed to get real GID\n"); + let _ = stderr.flush(); + exit(1) + } + } +} + +/// Gets the user name for a given user id. +/// +/// This function will read `/etc/passwd` looking for an entry for the provided +/// user ID, returning its UNIX username. In case of an error it will log message +/// to `stderr` and then will the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let user = get_user(1, &mut stderr); +/// +/// ``` +pub fn get_user(uid: usize, stderr: &mut Stderr) -> Option { + let mut passwd_string = String::new(); + let mut file = File::open(PASSWD_FILE).try(stderr); + file.read_to_string(&mut passwd_string).try(stderr); + + let passwd_file_entries = Passwd::parse_file(&passwd_string).unwrap(); + let passwd = passwd_file_entries.iter() + .find(|passwd| passwd.uid as usize == uid); + + match passwd { + Some(passwd) => Some(String::from(passwd.user)), + None => None + } +} + +/// Gets the UNIX group name for a given group ID. +/// +/// This function will read `/etc/group` file looking for an entry for the provided +/// group ID, returning its UNIX group name. In case of an error it will log message +/// to `stderr` and then will the caller process with an non-zero exit code. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let group = get_group(1, &mut stderr); +/// +/// ``` +pub fn get_group(gid: usize, stderr: &mut Stderr) -> Option { + let mut group_string = String::new(); + let mut file = File::open(GROUP_FILE).try(stderr); + file.read_to_string(&mut group_string).try(stderr); + + let group_file_entries = Group::parse_file(&group_string).unwrap(); + let group = group_file_entries.iter() + .find(|group| group.gid as usize == gid); + + match group { + Some(group) => Some(String::from(group.group)), + None => None + } }