use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
pub const MANAGER_PATH: &str = "/org/freedesktop/systemd1";
pub const MANAGER_INTERFACE: &str = "org.freedesktop.systemd1.Manager";
pub const EXCHANGE_WAIT: Duration = Duration::from_secs(20);
const BUS_NAME: &str = "org.freedesktop.DBus";
const BUS_PATH: &str = "/org/freedesktop/DBus";
pub const MANAGER_NAME: &str = "org.freedesktop.systemd1";
const MAX_REPLY_BYTES: usize = 1 << 20;
const METHOD_CALL: u8 = 1;
const METHOD_RETURN: u8 = 2;
const ERROR: u8 = 3;
const FIELD_REPLY_SERIAL: u8 = 5;
const FIELD_PATH: u8 = 1;
const FIELD_INTERFACE: u8 = 2;
const FIELD_MEMBER: u8 = 3;
const FIELD_ERROR_NAME: u8 = 4;
const FIELD_DESTINATION: u8 = 6;
const FIELD_SIGNATURE: u8 = 8;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CallError {
Connection(String),
Authentication(String),
Manager {
name: String,
text: String,
},
Malformed(String),
}
struct Writer {
bytes: Vec<u8>,
}
impl Writer {
fn align(&mut self, boundary: usize) {
while self.bytes.len() % boundary != 0 {
self.bytes.push(0);
}
}
fn unsigned(&mut self, value: u32) {
self.align(4);
self.bytes.extend(value.to_le_bytes());
}
fn text(&mut self, value: &str) {
self.unsigned(u32::try_from(value.len()).unwrap_or(u32::MAX));
self.bytes.extend(value.as_bytes());
self.bytes.push(0);
}
fn signature(&mut self, value: &str) {
self.bytes.push(u8::try_from(value.len()).unwrap_or(u8::MAX));
self.bytes.extend(value.as_bytes());
self.bytes.push(0);
}
fn field(&mut self, code: u8, kind: char, value: &str) {
self.align(8);
self.bytes.push(code);
self.signature(&kind.to_string());
if kind == 'g' {
self.signature(value);
} else {
self.text(value);
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Target<'a> {
pub destination: &'a str,
pub path: &'a str,
pub interface: &'a str,
pub member: &'a str,
}
pub const HELLO: Target<'static> = Target { destination: BUS_NAME, path: BUS_PATH, interface: BUS_NAME, member: "Hello" };
pub const RELOAD: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "Reload" };
pub const START_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StartUnit" };
pub const STOP_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "StopUnit" };
pub const RESET_FAILED_UNIT: Target<'static> =
Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "ResetFailedUnit" };
pub const GET_UNIT: Target<'static> = Target { destination: MANAGER_NAME, path: MANAGER_PATH, interface: MANAGER_INTERFACE, member: "GetUnit" };
const PROPERTIES_INTERFACE: &str = "org.freedesktop.DBus.Properties";
const UNIT_INTERFACE: &str = "org.freedesktop.systemd1.Unit";
pub const NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit";
const UNKNOWN_OBJECT: &str = "org.freedesktop.DBus.Error.UnknownObject";
pub fn encode_call(serial: u32, target: &Target<'_>, arguments: &[&str]) -> Vec<u8> {
let mut body: Writer = Writer { bytes: Vec::new() };
for argument in arguments {
body.text(argument);
}
let mut fields: Writer = Writer { bytes: vec![0; 16] };
fields.field(FIELD_PATH, 'o', target.path);
fields.field(FIELD_DESTINATION, 's', target.destination);
fields.field(FIELD_INTERFACE, 's', target.interface);
fields.field(FIELD_MEMBER, 's', target.member);
if !arguments.is_empty() {
fields.field(FIELD_SIGNATURE, 'g', &"s".repeat(arguments.len()));
}
let array_length: u32 = u32::try_from(fields.bytes.len() - 16).unwrap_or(u32::MAX);
fields.align(8);
let mut message: Vec<u8> = fields.bytes;
message[0] = b'l';
message[1] = METHOD_CALL;
message[2] = 0;
message[3] = 1;
message[4..8].copy_from_slice(&u32::try_from(body.bytes.len()).unwrap_or(u32::MAX).to_le_bytes());
message[8..12].copy_from_slice(&serial.to_le_bytes());
message[12..16].copy_from_slice(&array_length.to_le_bytes());
message.extend(body.bytes);
return message;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Reply {
pub kind: u8,
pub reply_serial: Option<u32>,
pub error_name: Option<String>,
pub first_text: Option<String>,
}
fn read_u32(bytes: &[u8], at: usize) -> Option<u32> {
return bytes.get(at..at.checked_add(4)?).map(|slice| { return u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]); });
}
fn read_text(bytes: &[u8], at: usize) -> Option<(String, usize)> {
let length: usize = usize::try_from(read_u32(bytes, at)?).ok()?;
let start: usize = at.checked_add(4)?;
let text: &[u8] = bytes.get(start..start.checked_add(length)?)?;
return Some((String::from_utf8_lossy(text).into_owned(), start.saturating_add(length).saturating_add(1)));
}
fn read_variant_text(bytes: &[u8], at: usize) -> Option<String> {
let length: usize = usize::from(*bytes.get(at)?);
let inner_start: usize = at.checked_add(1)?;
let inner: &[u8] = bytes.get(inner_start..inner_start.checked_add(length)?)?;
if inner != b"s" {
return None;
}
return read_text(bytes, aligned(inner_start.checked_add(length)?.checked_add(1)?, 4)).map(|(text, _)| { return text; });
}
fn aligned(offset: usize, boundary: usize) -> usize {
return offset.checked_add(boundary - 1).map_or(usize::MAX, |sum| { return sum / boundary * boundary; });
}
pub fn decode_reply(bytes: &[u8]) -> Result<Reply, CallError> {
let malformed = |what: &str| { return CallError::Malformed(what.to_string()); };
if bytes.len() < 16 || bytes[0] != b'l' {
return Err(malformed("the reply is not a little-endian message"));
}
let kind: u8 = bytes[1];
let body_length: usize = usize::try_from(read_u32(bytes, 4).ok_or_else(|| { return malformed("no body length"); })?).unwrap_or(usize::MAX);
let fields_length: usize = usize::try_from(read_u32(bytes, 12).ok_or_else(|| { return malformed("no header length"); })?).unwrap_or(usize::MAX);
let fields_end: usize = 16usize.saturating_add(fields_length);
let body_start: usize = aligned(fields_end, 8);
if bytes.len() < body_start.saturating_add(body_length) || fields_end > bytes.len() {
return Err(malformed("the reply is shorter than it says"));
}
let mut error_name: Option<String> = None;
let mut reply_serial: Option<u32> = None;
let mut signature: String = String::new();
let mut at: usize = 16;
while at < fields_end {
at = aligned(at, 8);
let (Some(&code), Some(&signature_length)) = (bytes.get(at), bytes.get(at.saturating_add(1))) else { break };
let kind_at: usize = at.saturating_add(2);
let Some(&value_kind) = bytes.get(kind_at) else { break };
let value_at: usize = kind_at.saturating_add(usize::from(signature_length)).saturating_add(1);
match (value_kind, code) {
(b's' | b'o', _) => {
let Some((text, next)) = read_text(bytes, aligned(value_at, 4)) else { break };
if code == FIELD_ERROR_NAME {
error_name = Some(text);
}
at = next;
}
(b'g', _) => {
let length: usize = usize::from(*bytes.get(value_at).unwrap_or(&0));
if code == FIELD_SIGNATURE {
let start: usize = value_at.saturating_add(1);
signature = String::from_utf8_lossy(bytes.get(start..start.saturating_add(length)).unwrap_or_default()).into_owned();
}
at = value_at.saturating_add(length).saturating_add(2);
}
(b'u', _) => {
let number_at: usize = aligned(value_at, 4);
if code == FIELD_REPLY_SERIAL {
reply_serial = read_u32(bytes, number_at);
}
at = number_at.saturating_add(4);
}
_ => break,
}
}
let first_text: Option<String> = if signature.starts_with('s') || signature.starts_with('o') {
read_text(bytes, aligned(body_start, 4)).map(|(text, _)| { return text; })
} else if signature == "v" {
read_variant_text(bytes, body_start)
} else {
None
};
return Ok(Reply { kind, reply_serial, error_name, first_text });
}
fn external_identity() -> String {
let user: u32 = unsafe { libc::geteuid() };
return user.to_string().bytes().map(|byte| { return format!("{byte:02x}"); }).collect();
}
fn remaining(deadline: Instant) -> Result<Duration, CallError> {
let left: Duration = deadline.saturating_duration_since(Instant::now());
return if left.is_zero() { Err(CallError::Connection(String::from("the bus did not answer in time"))) } else { Ok(left) };
}
fn read_line(stream: &mut UnixStream, deadline: Instant) -> Result<String, CallError> {
let mut line: Vec<u8> = Vec::new();
let mut one: [u8; 1] = [0];
while line.len() < 4096 {
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
stream.read_exact(&mut one).map_err(|error| { return CallError::Authentication(error.to_string()); })?;
if one[0] == b'\n' {
return Ok(String::from_utf8_lossy(line.as_slice()).trim_end_matches('\r').to_string());
}
line.push(one[0]);
}
return Err(CallError::Authentication(String::from("the answer is too long")));
}
fn read_message(stream: &mut UnixStream, deadline: Instant) -> Result<Vec<u8>, CallError> {
let mut header: Vec<u8> = vec![0; 16];
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
stream.read_exact(header.as_mut_slice()).map_err(|error| { return CallError::Connection(error.to_string()); })?;
let body_length: usize = usize::try_from(read_u32(header.as_slice(), 4).unwrap_or(0)).unwrap_or(usize::MAX);
let fields_length: usize = usize::try_from(read_u32(header.as_slice(), 12).unwrap_or(0)).unwrap_or(usize::MAX);
let rest: usize = aligned(16usize.saturating_add(fields_length), 8).saturating_add(body_length).saturating_sub(16);
if rest > MAX_REPLY_BYTES {
return Err(CallError::Malformed(String::from("the reply is larger than this client reads")));
}
let mut remainder: Vec<u8> = vec![0; rest];
stream.set_read_timeout(Some(remaining(deadline)?)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
stream.read_exact(remainder.as_mut_slice()).map_err(|error| { return CallError::Connection(error.to_string()); })?;
header.extend(remainder);
return Ok(header);
}
pub fn session_bus_path(address: Option<&str>, runtime_dir: Option<&Path>) -> Option<std::path::PathBuf> {
let from_address: Option<std::path::PathBuf> = address.and_then(|list| {
return list.split(';').find_map(|entry| {
let rest: &str = entry.strip_prefix("unix:")?;
let path: &str = rest.split(',').find_map(|option| { return option.strip_prefix("path="); })?;
return if path.contains('%') || path.is_empty() { None } else { Some(std::path::PathBuf::from(path)) };
});
});
return from_address.or_else(|| { return runtime_dir.map(|directory| { return directory.join("bus"); }); });
}
pub struct Bus {
stream: UnixStream,
deadline: Instant,
serial: u32,
}
impl Bus {
pub fn connect(socket: &Path, wait: Duration) -> Result<Bus, CallError> {
let deadline: Instant = Instant::now() + wait;
let mut stream: UnixStream = UnixStream::connect(socket).map_err(|error| { return CallError::Connection(error.to_string()); })?;
stream.set_write_timeout(Some(wait)).map_err(|error| { return CallError::Connection(error.to_string()); })?;
let mut hello: Vec<u8> = vec![0];
hello.extend(format!("AUTH EXTERNAL {}\r\n", external_identity()).as_bytes());
stream.write_all(hello.as_slice()).map_err(|error| { return CallError::Authentication(error.to_string()); })?;
let answer: String = read_line(&mut stream, deadline)?;
if !answer.starts_with("OK ") {
return Err(CallError::Authentication(answer));
}
stream.write_all(b"BEGIN\r\n").map_err(|error| { return CallError::Authentication(error.to_string()); })?;
let mut bus: Bus = Bus { stream, deadline, serial: 0 };
bus.call(&HELLO, &[])?;
return Ok(bus);
}
pub fn call(&mut self, target: &Target<'_>, arguments: &[&str]) -> Result<Reply, CallError> {
self.serial += 1;
let serial: u32 = self.serial;
self.stream.write_all(encode_call(serial, target, arguments).as_slice()).map_err(|error| { return CallError::Connection(error.to_string()); })?;
loop {
let reply: Reply = decode_reply(read_message(&mut self.stream, self.deadline)?.as_slice())?;
if reply.reply_serial != Some(serial) || !matches!(reply.kind, METHOD_RETURN | ERROR) {
continue;
}
return match (reply.kind, reply.error_name.clone()) {
(METHOD_RETURN, _) => Ok(reply),
(_, Some(name)) => Err(CallError::Manager { name, text: reply.first_text.unwrap_or_default() }),
_ => Err(CallError::Malformed(String::from("an error reply without a name"))),
};
}
}
pub fn reload(&mut self) -> Result<(), CallError> {
return self.call(&RELOAD, &[]).map(drop);
}
pub fn reset_failed(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&RESET_FAILED_UNIT, &[name]).map(drop);
}
pub fn stop_unit(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&STOP_UNIT, &[name, "replace"]).map(drop);
}
pub fn active_state(&mut self, name: &str) -> Result<Option<String>, CallError> {
let path: String = match self.call(&GET_UNIT, &[name]) {
Ok(reply) => reply.first_text.ok_or_else(|| { return CallError::Malformed(String::from("GetUnit answered without a path")); })?,
Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT => return Ok(None),
Err(error) => return Err(error),
};
let property: Target<'_> = Target { destination: MANAGER_NAME, path: path.as_str(), interface: PROPERTIES_INTERFACE, member: "Get" };
return match self.call(&property, &[UNIT_INTERFACE, "ActiveState"]) {
Ok(reply) => reply
.first_text
.map(Some)
.ok_or_else(|| { return CallError::Malformed(String::from("the state property answered without text")); }),
Err(CallError::Manager { name: error, .. }) if error == NO_SUCH_UNIT || error == UNKNOWN_OBJECT => Ok(None),
Err(error) => Err(error),
};
}
pub fn start_unit(&mut self, name: &str) -> Result<(), CallError> {
return self.call(&START_UNIT, &[name, "replace"]).map(drop);
}
}
#[cfg(test)]
#[path = "systemd_dbus_tests.rs"]
mod tests;