zbus/connection/handshake/
cookies.rs

1use std::{fmt, path::PathBuf};
2
3use futures_util::StreamExt;
4use tracing::trace;
5use xdg_home::home_dir;
6use zvariant::Str;
7
8use crate::{file::FileLines, Error, Result};
9
10#[derive(Debug)]
11pub(super) struct Cookie {
12    id: usize,
13    cookie: String,
14}
15
16impl Cookie {
17    #[cfg(feature = "p2p")]
18    pub fn id(&self) -> usize {
19        self.id
20    }
21
22    pub fn cookie(&self) -> &str {
23        &self.cookie
24    }
25
26    fn keyring_path() -> Result<PathBuf> {
27        let mut path = home_dir()
28            .ok_or_else(|| Error::Handshake("Failed to determine home directory".into()))?;
29        path.push(".dbus-keyrings");
30        Ok(path)
31    }
32
33    async fn read_keyring(context: &CookieContext<'_>) -> Result<Vec<Cookie>> {
34        let mut path = Cookie::keyring_path()?;
35        #[cfg(unix)]
36        {
37            use std::os::unix::fs::PermissionsExt;
38
39            let perms = crate::file::metadata(&path).await?.permissions().mode();
40            if perms & 0o066 != 0 {
41                return Err(Error::Handshake(
42                    "DBus keyring has invalid permissions".into(),
43                ));
44            }
45        }
46        #[cfg(not(unix))]
47        {
48            // FIXME: add code to check directory permissions
49        }
50        path.push(&*context.0);
51        trace!("Reading keyring {:?}", path);
52        let mut lines = FileLines::open(&path).await?.enumerate();
53        let mut cookies = vec![];
54        while let Some((n, line)) = lines.next().await {
55            let line = line?;
56            let mut split = line.split_whitespace();
57            let id = split
58                .next()
59                .ok_or_else(|| {
60                    Error::Handshake(format!(
61                        "DBus cookie `{}` missing ID at line {n}",
62                        path.display(),
63                    ))
64                })?
65                .parse()
66                .map_err(|e| {
67                    Error::Handshake(format!(
68                        "Failed to parse cookie ID in file `{}` at line {n}: {e}",
69                        path.display(),
70                    ))
71                })?;
72            let _ = split.next().ok_or_else(|| {
73                Error::Handshake(format!(
74                    "DBus cookie `{}` missing creation time at line {n}",
75                    path.display(),
76                ))
77            })?;
78            let cookie = split
79                .next()
80                .ok_or_else(|| {
81                    Error::Handshake(format!(
82                        "DBus cookie `{}` missing cookie data at line {}",
83                        path.to_str().unwrap(),
84                        n
85                    ))
86                })?
87                .to_string();
88            cookies.push(Cookie { id, cookie })
89        }
90        trace!("Loaded keyring {:?}", cookies);
91        Ok(cookies)
92    }
93
94    pub async fn lookup(context: &CookieContext<'_>, id: usize) -> Result<Cookie> {
95        let keyring = Self::read_keyring(context).await?;
96        keyring
97            .into_iter()
98            .find(|c| c.id == id)
99            .ok_or_else(|| Error::Handshake(format!("DBus cookie ID {id} not found")))
100    }
101
102    #[cfg(feature = "p2p")]
103    pub async fn first(context: &CookieContext<'_>) -> Result<Cookie> {
104        let keyring = Self::read_keyring(context).await?;
105        keyring
106            .into_iter()
107            .next()
108            .ok_or_else(|| Error::Handshake("No cookies available".into()))
109    }
110}
111
112#[derive(Debug)]
113pub struct CookieContext<'c>(Str<'c>);
114
115impl<'c> TryFrom<Str<'c>> for CookieContext<'c> {
116    type Error = Error;
117
118    fn try_from(value: Str<'c>) -> Result<Self> {
119        if value.is_empty() {
120            return Err(Error::Handshake("Empty cookie context".into()));
121        } else if !value.is_ascii() || value.contains(['/', '\\', ' ', '\n', '\r', '\t', '.']) {
122            return Err(Error::Handshake(
123                "Invalid characters in cookie context".into(),
124            ));
125        }
126
127        Ok(Self(value))
128    }
129}
130
131impl fmt::Display for CookieContext<'_> {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}", self.0)
134    }
135}
136
137impl Default for CookieContext<'_> {
138    fn default() -> Self {
139        Self(Str::from_static("org_freedesktop_general"))
140    }
141}
142
143impl From<hex::FromHexError> for Error {
144    fn from(e: hex::FromHexError) -> Self {
145        Error::Handshake(format!("Invalid hexcode: {e}"))
146    }
147}