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}
148