1use crate::process::Process;
40use crate::xml::XmlWriter;
41use std::ffi::{OsStr, OsString};
42use std::fs;
43use std::io;
44use std::os::unix::ffi::*;
45use std::os::unix::io::AsRawFd;
46use std::os::unix::net::UnixListener;
47use std::path::{Path, PathBuf};
48use std::time::Duration;
49
50mod pipe;
51mod process;
52mod sys;
53mod xml;
54
55#[derive(Clone, Debug)]
57pub struct Launcher {
58 program: Option<OsString>,
59 daemon_type: DaemonType,
60 config: Config,
61 services: Vec<Service>,
62}
63
64#[derive(Clone, Debug, Default)]
65struct Config {
66 bus_type: Option<BusType>,
67 allow_anonymous: bool,
68 listen: Vec<String>,
69 auth: Vec<Auth>,
70 service_dirs: Vec<PathBuf>,
71}
72
73
74#[non_exhaustive]
76#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
77pub enum DaemonType {
78 DBusDaemon,
80 DBusBroker,
82}
83
84#[derive(Clone, Debug)]
85struct Service {
86 name: String,
87 exec: PathBuf,
88}
89
90#[derive(Debug)]
94pub struct Daemon {
95 address: String,
96 tmp_dir: tempfile::TempDir,
97 process: Process,
98}
99
100#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
102pub enum Auth {
103 Anonymous,
104 External,
105 DBusCookieSha1,
106}
107
108#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
110pub enum BusType {
111 Session,
112 System,
113}
114
115impl Launcher {
116 pub fn new(daemon_type: DaemonType) -> Launcher {
118 Launcher {
119 program: None,
120 daemon_type,
121 config: Config::default(),
122 services: Vec::default(),
123 }
124 }
125
126 pub fn daemon() -> Launcher {
128 Self::new(DaemonType::DBusDaemon)
129 }
130
131 pub fn broker() -> Launcher {
133 Self::new(DaemonType::DBusBroker)
134 }
135
136 pub fn bus_type(&mut self, bus_type: BusType) -> &mut Self {
138 self.config.bus_type = Some(bus_type);
139 self
140 }
141
142 pub fn listen(&mut self, listen: &str) -> &mut Self {
155 self.config.listen.push(listen.to_owned());
156 self
157 }
158
159 pub fn allow_anonymous(&mut self) -> &mut Self {
164 self.config.allow_anonymous = true;
165 self
166 }
167
168 pub fn auth(&mut self, auth: Auth) -> &mut Self {
179 self.config.auth.push(auth);
180 self
181 }
182
183 pub fn service_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
185 let path = path.as_ref().to_path_buf();
186 self.config.service_dirs.push(path);
187 self
188 }
189
190 pub fn service<P: AsRef<Path>>(&mut self, name: &str, exec: P) -> &mut Self {
192 let name = name.to_string();
193 let exec = exec.as_ref().to_path_buf();
194 self.services.push(Service { name, exec });
195 self
196 }
197
198 #[doc(hidden)]
199 pub fn program(&mut self, program: &OsStr) -> &mut Self {
200 self.program = Some(program.to_owned());
201 self
202 }
203
204 pub fn launch(&self) -> io::Result<Daemon> {
206 let mut config = self.config.clone();
207
208 let tmp_dir = tempfile::Builder::new()
210 .prefix("dbus-daemon-rs-")
211 .tempdir()?;
212
213 if DaemonType::DBusDaemon == self.daemon_type && config.listen.is_empty() {
214 let path = escape_path(&tmp_dir.path());
218 let address = format!("unix:dir={}", &path);
219 config.listen.push(address);
220 }
221
222 if !self.services.is_empty() {
224 config.service_dirs.push(tmp_dir.path().to_owned());
225 for service in &self.services {
226 let file = format!("{}.service", service.name);
227 let path = tmp_dir.path().join(&file);
228 let contents = format!(
229 "[D-BUS Service]\nName={}\nExec={}\n",
230 service.name,
231 service.exec.display()
232 );
233 fs::write(path, contents)?;
234 }
235 }
236
237 let config_file = tmp_dir.path().join("daemon.conf");
239 fs::write(&config_file, config.to_xml().as_bytes())?;
240
241 let program = self.program.as_deref();
242 match self.daemon_type {
243 DaemonType::DBusDaemon => {
244 let (process, address) =
245 Process::spawn_dbus_daemon(program, &config_file)?;
246 Ok(Daemon {
247 address,
248 tmp_dir,
249 process,
250 })
251 }
252 DaemonType::DBusBroker => {
253 let path = tmp_dir.path().join("socket");
254 let address = format!("unix:path={}", escape_path(&path));
255 let socket = UnixListener::bind(&path)?;
256 let process = Process::spawn_dbus_broker(
257 program,
258 &config_file,
259 socket.as_raw_fd(),
260 )?;
261 Ok(Daemon {
262 address,
263 tmp_dir,
264 process,
265 })
266 }
267 }
268 }
269}
270
271fn escape_path(path: &Path) -> String {
272 use std::fmt::Write;
273
274 let mut escaped = String::new();
275 for b in path.as_os_str().as_bytes().iter().cloned() {
276 match b {
277 b'-'
278 | b'0'..=b'9'
279 | b'A'..=b'Z'
280 | b'a'..=b'z'
281 | b'_'
282 | b'/'
283 | b'.'
284 | b'\\' => {
285 escaped.push(b.into());
286 }
287 _ => {
288 write!(&mut escaped, "%{0:2x}", b).unwrap();
289 }
290 }
291 }
292
293 escaped
294}
295
296impl Daemon {
297 pub fn address(&self) -> &str {
299 &self.address
300 }
301
302 pub fn config_dir(&self) -> &Path {
306 self.tmp_dir.path()
307 }
308
309 pub fn pid(&self) -> libc::pid_t {
311 self.process.pid()
312 }
313}
314
315impl Drop for Daemon {
316 fn drop(&mut self) {
317 let _ = self.process.kill(libc::SIGTERM);
318 let _ = self.process.try_wait_timeout(Duration::from_secs(10));
319 let _ = self.process.kill(libc::SIGKILL);
320 let _ = self.process.wait();
321 }
322}
323
324impl Config {
325 fn to_xml(&self) -> String {
326 const DOCTYPE: &str = r#"<!DOCTYPE busconfig PUBLIC
327 "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
328 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">"#;
329
330 let mut s = String::new();
331 s.push_str(DOCTYPE);
332 s.push_str("\n");
333
334 let mut xml = XmlWriter::new(&mut s);
335 xml.start_tag("busconfig");
336
337 if let Some(bus_type) = self.bus_type {
338 xml.tag_with_text(
339 "type",
340 match bus_type {
341 BusType::Session => "session",
342 BusType::System => "system",
343 },
344 );
345 }
346
347 if self.allow_anonymous {
348 xml.start_tag("allow_anonymous");
349 xml.end_tag("allow_anonymous");
350 }
351
352 for listen in &self.listen {
353 xml.tag_with_text("listen", listen);
354 }
355
356 for auth in &self.auth {
357 xml.tag_with_text(
358 "auth",
359 match auth {
360 Auth::Anonymous => "ANONYMOUS",
361 Auth::External => "EXTERNAL",
362 Auth::DBusCookieSha1 => "DBUS_COOKIE_SHA1",
363 },
364 );
365 }
366
367 for dir in &self.service_dirs {
368 let dir = dir.to_str().expect("servicedir is not valid UTF-8");
369 xml.tag_with_text("servicedir", dir);
370 }
371
372 xml.start_tag("policy");
373 xml.attr("context", "default");
374
375 xml.start_tag("allow");
376 xml.attr("receive_requested_reply", "true");
377 xml.end_tag("allow");
378
379 xml.start_tag("allow");
380 xml.attr("send_destination", "*");
381 xml.end_tag("allow");
382
383 xml.start_tag("allow");
384 xml.attr("own", "*");
385 xml.end_tag("allow");
386
387 xml.end_tag("policy");
388
389 xml.end_tag("busconfig");
390
391 s
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
401 fn to_xml() {
402 let mut c = Config::default();
403 c.bus_type = Some(BusType::Session);
404 c.listen.push("unix:tmpdir=/tmp".into());
405 c.auth.push(Auth::Anonymous);
406 c.auth.push(Auth::External);
407 c.auth.push(Auth::DBusCookieSha1);
408 c.service_dirs.push("/tmp/servicedir".into());
409
410 let actual = c.to_xml();
411 let expected = r#"<!DOCTYPE busconfig PUBLIC
412 "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
413 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
414<busconfig>
415 <type>session</type>
416 <listen>unix:tmpdir=/tmp</listen>
417 <auth>ANONYMOUS</auth>
418 <auth>EXTERNAL</auth>
419 <auth>DBUS_COOKIE_SHA1</auth>
420 <servicedir>/tmp/servicedir</servicedir>
421 <policy context="default">
422 <allow receive_requested_reply="true"/>
423 <allow send_destination="*"/>
424 <allow own="*"/>
425 </policy>
426</busconfig>
427"#;
428
429 assert_eq!(expected, actual, "\n\n{}.\n\n{}.", expected, actual);
430 }
431
432 #[test]
433 fn escape() {
434 assert_eq!("/", &escape_path(Path::new("/")));
435 assert_eq!("/tmp/a%23b", &escape_path(Path::new("/tmp/a#b")));
436 }
437}