blob: 0d4b1ff627308e81affd11d746f2d54b19efb527 [file] [log] [blame] [edit]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Provides helper functions used by handler implementations of crosh commands.
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::env;
use std::error;
use std::fmt::{self, Display};
use std::fs::read_to_string;
use std::io::stdin;
use std::io::stdout;
use std::io::Read;
use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use chrono::Local;
use dbus::blocking::Connection;
use libc::{c_int, epoll_event};
use libchromeos::chromeos;
use libchromeos::signal::{clear_signal_handler, register_signal_handler};
use log::error;
use nix::sys::signal::Signal;
use system_api::client::OrgChromiumSessionManagerInterface;
// 25 seconds is the default timeout for dbus-send.
pub const DEFAULT_DBUS_TIMEOUT: Duration = Duration::from_secs(25);
// Path to update_engine_client.
pub const UPDATE_ENGINE: &str = "/usr/bin/update_engine_client";
const CROS_USER_ID_HASH: &str = "CROS_USER_ID_HASH";
// 30 seconds is the default timeout for epoll.
const DEFAULT_EPOLL_TIMEOUT: i32 = 30000;
const MAX_EPOLL_EVENTS: i32 = 1;
static INCLUDE_DEV: AtomicBool = AtomicBool::new(false);
static INCLUDE_USB: AtomicBool = AtomicBool::new(false);
#[derive(Debug)]
pub enum Error {
DbusChromeFeaturesService(dbus::Error, String),
DbusConnection(dbus::Error),
DbusGetUserIdHash(chromeos::Error),
DbusGuestSessionActive(dbus::Error),
EpollAdd(i32),
EpollCreate(i32),
NoMatchFound,
WrappedError(String),
}
pub type Result<T> = std::result::Result<T, Error>;
impl Display for Error {
#[remain::check]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
#[sorted]
match self {
DbusChromeFeaturesService(err, m) => write!(f, "failed to call '{}': {}", m, err),
DbusConnection(err) => write!(f, "failed to connect to D-Bus: {}", err),
DbusGetUserIdHash(err) => {
write!(f, "failed to get user-id hash over to D-Bus: {:?}", err)
}
DbusGuestSessionActive(err) => {
write!(
f,
"failed to check whether guest session is active over D-Bus: {:?}",
err
)
}
EpollAdd(err) => write!(f, "failed to add to epoll: {}", err),
EpollCreate(err) => write!(f, "failed to create epoll: {}", err),
NoMatchFound => write!(f, "No match found."),
WrappedError(err) => write!(f, "{}", err),
}
}
}
impl<T: error::Error> From<T> for Error {
fn from(err: T) -> Self {
Error::WrappedError(format!("{:?}", err))
}
}
// Return the user ID hash from the environment. If it is not available, fetch it from session
// manager and set the environment variable.
pub fn get_user_id_hash() -> Result<String> {
if let Ok(lookup) = env::var(CROS_USER_ID_HASH) {
return Ok(lookup);
}
let user_id_hash = chromeos::get_user_id_hash().map_err(|err| {
error!("ERROR: D-Bus call failed: {}", err);
Error::DbusGetUserIdHash(err)
})?;
env::set_var(CROS_USER_ID_HASH, &user_id_hash);
Ok(user_id_hash)
}
// Return the output file path for the given output type in user's Downloads directory.
pub fn generate_output_file_path(output_type: &str, file_extension: &str) -> Result<String> {
let date = Local::now();
let formatted_date = date.format("%Y-%m-%d_%H.%M.%S");
let user_id_hash = get_user_id_hash()?;
let random_string: String = thread_rng().sample_iter(&Alphanumeric).take(6).collect();
Ok(format!(
"/home/user/{}/MyFiles/Downloads/{}_{}_{}.{}",
user_id_hash, output_type, formatted_date, random_string, file_extension
))
}
pub fn is_chrome_feature_enabled(method_name: &str) -> Result<bool> {
let user_id_hash = get_user_id_hash()?;
let connection = Connection::new_system().map_err(|err| {
error!("ERROR: Failed to get D-Bus connection: {}", err);
Error::DbusConnection(err)
})?;
let proxy = connection.with_proxy(
"org.chromium.ChromeFeaturesService",
"/org/chromium/ChromeFeaturesService",
DEFAULT_DBUS_TIMEOUT,
);
let (reply,): (bool,) = proxy
.method_call(
"org.chromium.ChromeFeaturesServiceInterface",
method_name,
(user_id_hash,),
)
.map_err(|err| {
error!("ERROR: D-Bus method call failed: {}", err);
Error::DbusChromeFeaturesService(err, method_name.to_string())
})?;
Ok(reply)
}
pub fn is_no_new_privs_set() -> bool {
// Safe because this retrieves a value without side effects.
unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) > 0 }
}
pub fn is_removable() -> Result<bool> {
let dev = root_dev()?;
let dev = blockdev_basename(&dev).ok_or(Error::NoMatchFound)?;
match read_to_string(format!("/sys/block/{}/removable", dev)) {
Ok(contents) => Ok(contents.trim() == "1"),
Err(err) => Err(err.into()),
}
}
pub fn is_consumer_device() -> Result<bool> {
let output = Command::new("/usr/sbin/device_management_client")
.arg("--action=enterprise_owned_get_status")
.output()?;
let stdout = std::str::from_utf8(&output.stdout)
.unwrap()
.trim()
.to_string();
match output.status.code() {
Some(0) if stdout == "EnterpriseOwnedGetStatus(): 0" => Ok(true),
Some(0) if stdout == "EnterpriseOwnedGetStatus(): 1" => Ok(false),
Some(0) => Err(Error::WrappedError(stdout)),
Some(v) => Err(Error::WrappedError(format!("unexpected exit code: {}", v))),
None => Err(Error::WrappedError("failed to get exit code".to_string())),
}
}
pub fn set_dev_commands_included(value: bool) {
INCLUDE_DEV.store(value, Ordering::Release);
}
pub fn set_usb_commands_included(value: bool) {
INCLUDE_USB.store(value, Ordering::Release);
}
pub fn dev_commands_included() -> bool {
INCLUDE_DEV.load(Ordering::Acquire)
}
pub fn usb_commands_included() -> bool {
INCLUDE_USB.load(Ordering::Acquire)
}
/// # Safety
/// handler needs to be async safe.
pub unsafe fn set_signal_handlers(signums: &[Signal], handler: extern "C" fn(c_int)) {
for signum in signums {
// Safe as long as handler is async safe.
if unsafe { register_signal_handler(*signum, handler) }.is_err() {
error!("sigaction failed for {}", signum);
}
}
}
pub fn clear_signal_handlers(signums: &[Signal]) {
for signum in signums {
if clear_signal_handler(*signum).is_err() {
error!("sigaction failed for {}", signum);
}
}
}
pub fn is_guest_session_active() -> Result<bool> {
let connection = Connection::new_system().map_err(|err| {
error!("ERROR: Failed to get D-Bus connection: {}", err);
Error::DbusConnection(err)
})?;
let conn_path = connection.with_proxy(
"org.chromium.SessionManager",
"/org/chromium/SessionManager",
DEFAULT_DBUS_TIMEOUT,
);
let guest_session_active = conn_path.is_guest_session_active().map_err(|err| {
println!("ERROR: Got unexpected result: {}", err);
Error::DbusGuestSessionActive(err)
})?;
Ok(guest_session_active)
}
fn root_dev() -> Result<String> {
let mut child = Command::new("rootdev")
.arg("-s")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
let mut result = String::new();
child.stdout.take().unwrap().read_to_string(&mut result)?;
child.wait()?;
Ok(result.trim().to_string())
}
/// Given a path to a block device with partition number, return its name.
///
/// For example: "/dev/nvme0n1p1" -> "nvme0n1"
fn blockdev_basename(mut dev: &str) -> Option<&str> {
dev = dev.strip_prefix("/dev/")?;
// The device name should be a single path component.
if dev.contains('/') {
return None;
}
// Remove partition number.
if let Some((base, part)) = dev.rsplit_once('p') {
// nvme0n1p3 -> nvme0n1
if part.chars().all(|c: char| c.is_ascii_digit()) {
return Some(base);
}
}
// sda1 -> sda
Some(dev.trim_end_matches(|c: char| c.is_ascii_digit()))
}
/// Print 'msg' followed by a [y/N] prompt and test the user input. Return true for 'y' or 'Y'.
pub fn prompt_for_yes(msg: &str) -> bool {
print!("{} [y/N] ", msg);
stdout().flush().ok();
let mut response = String::new();
stdin().read_line(&mut response).ok();
matches!(response.as_str(), "y\n" | "Y\n")
}
// Creates a new epoll instance watching the provided fd.
pub fn add_epoll_for_fd(fd: i32) -> Result<i32> {
// Safe because this will create a new epoll instance. Then we check the
// return value.
let epoll_fd = unsafe { libc::epoll_create1(0) };
if epoll_fd < 0 {
error!(
"ERROR: Got unexpected result from epoll_create1(): {}",
epoll_fd
);
return Err(Error::EpollCreate(epoll_fd));
}
let mut evt = epoll_event { events: 0, u64: 0 };
// Safe because this only adds an entry to the epoll interest list. Then we
// check the return value.
let add_result = unsafe { libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, fd, &mut evt) };
if add_result != 0 {
error!("ERROR: Got unexpected epoll result: {}", add_result);
return Err(Error::EpollAdd(add_result));
}
Ok(epoll_fd)
}
// Waits for an epoll event on the provided fd.
//
// The return value of the underlying syscall is returned.
pub fn epoll_wait(epoll_fd: i32) -> i32 {
let mut evt = epoll_event { events: 0, u64: 0 };
// Safe because we give an epoll_event array that can hold MAX_EPOLL_EVENTS.
unsafe { libc::epoll_wait(epoll_fd, &mut evt, MAX_EPOLL_EVENTS, DEFAULT_EPOLL_TIMEOUT) }
}
#[cfg(test)]
mod tests {
use regex::Regex;
use super::*;
#[test]
fn test_generate_output_file_path() {
// Set the user id hash env variable to a random value because it is necessary for output path generation.
env::set_var(CROS_USER_ID_HASH, "useridhashfortesting");
let expected_path_re = Regex::new(concat!(
r"^/home/user/.+/MyFiles/Downloads/",
r"packet_capture_\d{4}-\d{2}-\d{2}_\d{2}.\d{2}.\d{2}_.{6}\.pcap$"
))
.unwrap();
let result_output_path = generate_output_file_path("packet_capture", "pcap").unwrap();
assert!(expected_path_re.is_match(&result_output_path));
}
#[test]
fn blockdev_basename_valid() {
assert_eq!(blockdev_basename("/dev/mmcblk0p3"), Some("mmcblk0"));
assert_eq!(blockdev_basename("/dev/nvme0n1p1"), Some("nvme0n1"));
assert_eq!(blockdev_basename("/dev/sda1"), Some("sda"));
}
#[test]
fn blockdev_basename_invalid() {
assert!(blockdev_basename("abcdef").is_none());
assert!(blockdev_basename("/dev/abc/def").is_none());
}
}