use crate::error::{FezError, Result};
use crate::protocol::frame::{read_frame, write_frame, Frame};
use crate::protocol::message::{Control, DbusCall, DbusResponse, DbusSignal, IncomingControl};
use crate::transport::Transport;
use serde_json::{json, Value};
use std::process::{Child, Stdio};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::thread;
use std::time::Duration;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const SUPERUSER_PATH: &str = "/superuser";
const SUPERUSER_IFACE: &str = "cockpit.Superuser";
const ESCALATION_REMEDIATION: &str = "fez could not escalate to root: no superuser mechanism succeeded. Install the cockpit-system package (it ships the sudo/pkexec superuser bridge definitions), then either configure passwordless sudo (NOPASSWD) for this user or grant a polkit rule allowing this user the privileged cockpit action, and retry. fez does not supply sudo passwords";
pub struct BridgeClient {
child: Child,
stdin: std::process::ChildStdin,
rx: Receiver<Frame>,
host: String,
next_channel: u64,
escalated: bool,
}
impl BridgeClient {
pub fn connect(transport: &dyn Transport) -> Result<BridgeClient> {
let mut cmd = transport.command();
let program = cmd.get_program().to_string_lossy().into_owned();
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|source| FezError::Spawn { program, source })?;
let stdin = child.stdin.take().expect("piped stdin");
let mut stdout = child.stdout.take().expect("piped stdout");
let (tx, rx) = mpsc::channel::<Frame>();
thread::spawn(move || {
while let Ok(Some(frame)) = read_frame(&mut stdout) {
if tx.send(frame).is_err() {
break;
}
}
});
let mut client = BridgeClient {
child,
stdin,
rx,
host: transport.host_label(),
next_channel: 1,
escalated: false,
};
client.send_control(&Control::Init {
version: 1,
host: "localhost".into(),
superuser: Some(json!("none")),
})?;
client.await_init()?;
Ok(client)
}
fn send_control(&mut self, c: &Control) -> Result<()> {
write_frame(&mut self.stdin, &Frame::control(&c.to_json())).map_err(FezError::Io)
}
fn recv(&self) -> Result<Frame> {
match self.rx.recv_timeout(DEFAULT_TIMEOUT) {
Ok(f) => Ok(f),
Err(RecvTimeoutError::Timeout) => Err(FezError::Timeout),
Err(RecvTimeoutError::Disconnected) => Err(FezError::BridgeClosed),
}
}
fn await_init(&mut self) -> Result<()> {
loop {
let frame = self.recv()?;
if !frame.channel.is_empty() {
continue;
}
let c: IncomingControl =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if c.command == "init" {
return Ok(());
}
}
}
fn alloc_channel(&mut self) -> String {
let c = format!("c{}", self.next_channel);
self.next_channel += 1;
c
}
pub fn dbus_open(&mut self, name: &str) -> Result<String> {
self.open_dbus(name, false)
}
pub fn dbus_open_privileged(&mut self, name: &str) -> Result<String> {
self.open_dbus(name, true)
}
fn open_dbus(&mut self, name: &str, privileged: bool) -> Result<String> {
if privileged && !self.escalated {
self.escalate()?;
}
let channel = self.alloc_channel();
let mut open = Control::open(&channel, "dbus-json3")
.opt("bus", json!("system"))
.opt("name", json!(name));
if privileged {
open = open.opt("superuser", json!("require"));
}
self.send_control(&open)?;
Ok(channel)
}
pub fn escalate(&mut self) -> Result<()> {
if self.escalated {
return Ok(());
}
let denied = || FezError::AccessDenied {
remediation: ESCALATION_REMEDIATION.into(),
};
match std::env::var("FEZ_ESCALATION").ok().as_deref() {
Some("off") => return Err(denied()),
Some(name) if !name.is_empty() => {
return match self.superuser_start(name) {
Ok(()) => {
self.escalated = true;
Ok(())
}
Err(FezError::Dbus { .. }) => Err(denied()),
Err(e) => Err(e),
};
}
_ => {}
}
let names = self.superuser_bridges()?;
for name in names {
match self.superuser_start(&name) {
Ok(()) => {
self.escalated = true;
return Ok(());
}
Err(FezError::Dbus { .. }) => continue,
Err(e) => return Err(e),
}
}
Err(denied())
}
fn open_dbus_internal(&mut self) -> Result<String> {
let channel = self.alloc_channel();
let open = Control::open(&channel, "dbus-json3").opt("bus", json!("internal"));
self.send_control(&open)?;
Ok(channel)
}
pub fn superuser_bridges(&mut self) -> Result<Vec<String>> {
let channel = self.open_dbus_internal()?;
let out = self.dbus_call(
&channel,
SUPERUSER_PATH,
"org.freedesktop.DBus.Properties",
"Get",
json!([SUPERUSER_IFACE, "Bridges"]),
)?;
let names = out
.as_array()
.and_then(|args| args.first())
.map(variant_value)
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
Ok(names)
}
pub fn superuser_start(&mut self, name: &str) -> Result<()> {
let channel = self.open_dbus_internal()?;
self.dbus_call(
&channel,
SUPERUSER_PATH,
SUPERUSER_IFACE,
"Start",
json!([name]),
)?;
Ok(())
}
pub fn dbus_call(
&mut self,
channel: &str,
path: &str,
iface: &str,
method: &str,
args: Value,
) -> Result<Value> {
let call = DbusCall::new(channel, path, iface, method, args);
write_frame(&mut self.stdin, &Frame::new(channel, call.to_json())).map_err(FezError::Io)?;
loop {
let frame = self.recv()?;
if frame.channel.is_empty() {
let c: IncomingControl =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if c.command == "close" && c.channel.as_deref() == Some(channel) {
return Err(close_problem_to_error(c.problem));
}
continue;
}
if frame.channel != channel {
continue;
}
let resp: DbusResponse =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if resp.id.as_deref() != Some(&call.id) {
continue; }
if let Some(name) = resp.dbus_error_name() {
return Err(FezError::Dbus {
name: name.into(),
message: resp.dbus_error_message().unwrap_or_default(),
});
}
return Ok(resp.out_args().cloned().unwrap_or(Value::Null));
}
}
pub fn dbus_call_collect(
&mut self,
channel: &str,
path: &str,
iface: &str,
method: &str,
args: Value,
) -> Result<Vec<(String, Vec<Value>)>> {
let call = DbusCall::new(channel, path, iface, method, args);
write_frame(&mut self.stdin, &Frame::new(channel, call.to_json())).map_err(FezError::Io)?;
let mut collected: Vec<(String, Vec<Value>)> = Vec::new();
loop {
let frame = self.recv()?;
if frame.channel.is_empty() {
let c: IncomingControl =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if c.command == "close" && c.channel.as_deref() == Some(channel) {
return Err(close_problem_to_error(c.problem));
}
continue;
}
if frame.channel != channel {
continue;
}
let sig: DbusSignal =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
let Some(member) = sig.member() else {
continue;
};
if sig.path() != Some(path) {
continue; }
let member = member.to_string();
let args = sig.args().cloned().unwrap_or_default();
let finished = member == "Finished";
collected.push((member, args));
if finished {
return Ok(collected);
}
}
}
pub fn stream_collect(&mut self, argv: &[&str]) -> Result<Vec<u8>> {
let channel = self.alloc_channel();
self.send_control(&Control::open(&channel, "stream").opt("spawn", json!(argv)))?;
let mut buf = Vec::new();
loop {
let frame = self.recv()?;
if frame.channel == channel {
buf.extend_from_slice(&frame.payload);
} else if frame.channel.is_empty() {
let c: IncomingControl =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if c.channel.as_deref() == Some(&channel) {
if c.command == "close" && c.problem.is_some() {
return Err(close_problem_to_error(c.problem));
}
if c.command == "done" || c.command == "close" {
return Ok(buf);
}
}
}
}
}
pub fn stream_each<F: FnMut(&[u8])>(&mut self, argv: &[&str], mut on_chunk: F) -> Result<()> {
let channel = self.alloc_channel();
self.send_control(&Control::open(&channel, "stream").opt("spawn", json!(argv)))?;
loop {
let frame = self.recv()?;
if frame.channel == channel {
on_chunk(&frame.payload);
} else if frame.channel.is_empty() {
let c: IncomingControl =
serde_json::from_slice(&frame.payload).map_err(FezError::Decode)?;
if c.channel.as_deref() == Some(&channel) {
if c.command == "close" && c.problem.is_some() {
return Err(close_problem_to_error(c.problem));
}
if c.command == "done" || c.command == "close" {
return Ok(());
}
}
}
}
}
pub fn host(&self) -> &str {
&self.host
}
}
impl Drop for BridgeClient {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn variant_value(v: &Value) -> &Value {
v.get("v").unwrap_or(v)
}
fn close_problem_to_error(problem: Option<String>) -> FezError {
match problem {
Some(p) if p == "access-denied" => FezError::AccessDenied {
remediation: ESCALATION_REMEDIATION.into(),
},
Some(p) => FezError::Problem(p),
None => FezError::Problem("channel-closed".into()),
}
}