| 1 | use std::{fmt, path::PathBuf}; |
| 2 | |
| 3 | use futures_util::StreamExt; |
| 4 | use tracing::trace; |
| 5 | use xdg_home::home_dir; |
| 6 | use zvariant::Str; |
| 7 | |
| 8 | use crate::{file::FileLines, Error, Result}; |
| 9 | |
| 10 | #[derive (Debug)] |
| 11 | pub(super) struct Cookie { |
| 12 | id: usize, |
| 13 | cookie: String, |
| 14 | } |
| 15 | |
| 16 | impl 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)] |
| 113 | pub struct CookieContext<'c>(Str<'c>); |
| 114 | |
| 115 | impl<'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 | |
| 131 | impl fmt::Display for CookieContext<'_> { |
| 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 133 | write!(f, " {}" , self.0) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | impl Default for CookieContext<'_> { |
| 138 | fn default() -> Self { |
| 139 | Self(Str::from_static("org_freedesktop_general" )) |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | impl From<hex::FromHexError> for Error { |
| 144 | fn from(e: hex::FromHexError) -> Self { |
| 145 | Error::Handshake(format!("Invalid hexcode: {e}" )) |
| 146 | } |
| 147 | } |
| 148 | |