| 1 | use async_trait::async_trait; |
| 2 | use std::collections::VecDeque; |
| 3 | use tracing::{debug, instrument, trace, warn}; |
| 4 | |
| 5 | use sha1::{Digest, Sha1}; |
| 6 | |
| 7 | use crate::{conn::socket::ReadHalf, is_flatpak, names::OwnedUniqueName, Message}; |
| 8 | |
| 9 | use super::{ |
| 10 | random_ascii, sasl_auth_id, AuthMechanism, Authenticated, BoxedSplit, Command, Common, Cookie, |
| 11 | Error, Handshake, OwnedGuid, Result, Str, |
| 12 | }; |
| 13 | |
| 14 | /// A representation of an in-progress handshake, client-side |
| 15 | /// |
| 16 | /// This struct is an async-compatible representation of the initial handshake that must be |
| 17 | /// performed before a D-Bus connection can be used. |
| 18 | #[derive (Debug)] |
| 19 | pub struct Client { |
| 20 | common: Common, |
| 21 | server_guid: Option<OwnedGuid>, |
| 22 | bus: bool, |
| 23 | } |
| 24 | |
| 25 | impl Client { |
| 26 | /// Start a handshake on this client socket |
| 27 | pub fn new( |
| 28 | socket: BoxedSplit, |
| 29 | mechanisms: Option<VecDeque<AuthMechanism>>, |
| 30 | server_guid: Option<OwnedGuid>, |
| 31 | bus: bool, |
| 32 | ) -> Client { |
| 33 | let mechanisms = mechanisms.unwrap_or_else(|| { |
| 34 | let mut mechanisms = VecDeque::new(); |
| 35 | mechanisms.push_back(AuthMechanism::External); |
| 36 | mechanisms.push_back(AuthMechanism::Cookie); |
| 37 | mechanisms.push_back(AuthMechanism::Anonymous); |
| 38 | mechanisms |
| 39 | }); |
| 40 | |
| 41 | Client { |
| 42 | common: Common::new(socket, mechanisms), |
| 43 | server_guid, |
| 44 | bus, |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /// Respond to a cookie authentication challenge from the server. |
| 49 | /// |
| 50 | /// Returns the next command to send to the server. |
| 51 | async fn handle_cookie_challenge(&mut self, data: Vec<u8>) -> Result<Command> { |
| 52 | let context = std::str::from_utf8(&data) |
| 53 | .map_err(|_| Error::Handshake("Cookie context was not valid UTF-8" .into()))?; |
| 54 | let mut split = context.split_ascii_whitespace(); |
| 55 | let context = split |
| 56 | .next() |
| 57 | .ok_or_else(|| Error::Handshake("Missing cookie context name" .into()))?; |
| 58 | let context = Str::from(context).try_into()?; |
| 59 | let id = split |
| 60 | .next() |
| 61 | .ok_or_else(|| Error::Handshake("Missing cookie ID" .into()))?; |
| 62 | let id = id |
| 63 | .parse() |
| 64 | .map_err(|e| Error::Handshake(format!("Invalid cookie ID ` {id}`: {e}" )))?; |
| 65 | let server_challenge = split |
| 66 | .next() |
| 67 | .ok_or_else(|| Error::Handshake("Missing cookie challenge" .into()))?; |
| 68 | |
| 69 | let cookie = Cookie::lookup(&context, id).await?; |
| 70 | let cookie = cookie.cookie(); |
| 71 | let client_challenge = random_ascii(16); |
| 72 | let sec = format!(" {server_challenge}: {client_challenge}: {cookie}" ); |
| 73 | let sha1 = hex::encode(Sha1::digest(sec)); |
| 74 | let data = format!(" {client_challenge} {sha1}" ).into_bytes(); |
| 75 | |
| 76 | Ok(Command::Data(Some(data))) |
| 77 | } |
| 78 | |
| 79 | fn set_guid(&mut self, guid: OwnedGuid) -> Result<()> { |
| 80 | match &self.server_guid { |
| 81 | Some(server_guid) if *server_guid != guid => { |
| 82 | return Err(Error::Handshake(format!( |
| 83 | "Server GUID mismatch: expected {server_guid}, got {guid}" , |
| 84 | ))); |
| 85 | } |
| 86 | Some(_) => (), |
| 87 | None => self.server_guid = Some(guid), |
| 88 | } |
| 89 | |
| 90 | Ok(()) |
| 91 | } |
| 92 | |
| 93 | // The dbus daemon on some platforms requires sending the zero byte as a |
| 94 | // separate message with SCM_CREDS. |
| 95 | #[instrument (skip(self))] |
| 96 | #[cfg (any(target_os = "freebsd" , target_os = "dragonfly" ))] |
| 97 | async fn send_zero_byte(&mut self) -> Result<()> { |
| 98 | let write = self.common.socket_mut().write_mut(); |
| 99 | |
| 100 | let written = match write.send_zero_byte().await.map_err(|e| { |
| 101 | Error::Handshake(format!("Could not send zero byte with credentials: {}" , e)) |
| 102 | })? { |
| 103 | // This likely means that the socket type is unable to send SCM_CREDS. |
| 104 | // Let's try to send the 0 byte as a regular message. |
| 105 | None => write.sendmsg(&[0], &[]).await?, |
| 106 | Some(n) => n, |
| 107 | }; |
| 108 | |
| 109 | if written != 1 { |
| 110 | return Err(Error::Handshake( |
| 111 | "Could not send zero byte with credentials" .to_string(), |
| 112 | )); |
| 113 | } |
| 114 | |
| 115 | Ok(()) |
| 116 | } |
| 117 | |
| 118 | /// Perform the authentication handshake with the server. |
| 119 | /// |
| 120 | /// In case of cookie auth, it returns the challenge response to send to the server, so it can |
| 121 | /// be batched with rest of the commands. |
| 122 | #[instrument (skip(self))] |
| 123 | async fn authenticate(&mut self) -> Result<Option<Command>> { |
| 124 | loop { |
| 125 | let mechanism = self.common.next_mechanism()?; |
| 126 | trace!("Trying {mechanism} mechanism" ); |
| 127 | let auth_cmd = match mechanism { |
| 128 | AuthMechanism::Anonymous => Command::Auth(Some(mechanism), Some("zbus" .into())), |
| 129 | AuthMechanism::External => { |
| 130 | Command::Auth(Some(mechanism), Some(sasl_auth_id()?.into_bytes())) |
| 131 | } |
| 132 | AuthMechanism::Cookie => Command::Auth( |
| 133 | Some(AuthMechanism::Cookie), |
| 134 | Some(sasl_auth_id()?.into_bytes()), |
| 135 | ), |
| 136 | }; |
| 137 | self.common.write_command(auth_cmd).await?; |
| 138 | |
| 139 | match self.common.read_command().await? { |
| 140 | Command::Ok(guid) => { |
| 141 | trace!("Received OK from server" ); |
| 142 | self.set_guid(guid)?; |
| 143 | |
| 144 | return Ok(None); |
| 145 | } |
| 146 | Command::Data(data) if mechanism == AuthMechanism::Cookie => { |
| 147 | let data = data.ok_or_else(|| { |
| 148 | Error::Handshake("Received DATA with no data from server" .into()) |
| 149 | })?; |
| 150 | trace!("Received cookie challenge from server" ); |
| 151 | let response = self.handle_cookie_challenge(data).await?; |
| 152 | |
| 153 | return Ok(Some(response)); |
| 154 | } |
| 155 | Command::Rejected(_) => debug!(" {mechanism} rejected by the server" ), |
| 156 | Command::Error(e) => debug!("Received error from server: {e}" ), |
| 157 | cmd => { |
| 158 | return Err(Error::Handshake(format!( |
| 159 | "Unexpected command from server: {cmd}" |
| 160 | ))) |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /// Sends out all commands after authentication. |
| 167 | /// |
| 168 | /// This includes the challenge response for cookie auth, if any and returns the number of |
| 169 | /// responses expected from the server. |
| 170 | #[instrument (skip(self))] |
| 171 | async fn send_secondary_commands( |
| 172 | &mut self, |
| 173 | challenge_response: Option<Command>, |
| 174 | ) -> Result<usize> { |
| 175 | let mut commands = Vec::with_capacity(4); |
| 176 | if let Some(response) = challenge_response { |
| 177 | commands.push(response); |
| 178 | } |
| 179 | |
| 180 | let can_pass_fd = self.common.socket_mut().read_mut().can_pass_unix_fd(); |
| 181 | if can_pass_fd { |
| 182 | // xdg-dbus-proxy can't handle pipelining, hence this special handling. |
| 183 | // FIXME: Remove this as soon as flatpak is fixed and fix is available in major distros. |
| 184 | // See https://github.com/flatpak/xdg-dbus-proxy/issues/21 |
| 185 | if is_flatpak() { |
| 186 | self.common.write_command(Command::NegotiateUnixFD).await?; |
| 187 | match self.common.read_command().await? { |
| 188 | Command::AgreeUnixFD => self.common.set_cap_unix_fd(true), |
| 189 | Command::Error(e) => warn!("UNIX file descriptor passing rejected: {e}" ), |
| 190 | cmd => { |
| 191 | return Err(Error::Handshake(format!( |
| 192 | "Unexpected command from server: {cmd}" |
| 193 | ))) |
| 194 | } |
| 195 | } |
| 196 | } else { |
| 197 | commands.push(Command::NegotiateUnixFD); |
| 198 | } |
| 199 | }; |
| 200 | commands.push(Command::Begin); |
| 201 | let hello_method = if self.bus { |
| 202 | Some(create_hello_method_call()) |
| 203 | } else { |
| 204 | None |
| 205 | }; |
| 206 | |
| 207 | self.common |
| 208 | .write_commands(&commands, hello_method.as_ref().map(|m| &**m.data())) |
| 209 | .await?; |
| 210 | |
| 211 | // Server replies to all commands except `BEGIN`. |
| 212 | Ok(commands.len() - 1) |
| 213 | } |
| 214 | |
| 215 | #[instrument (skip(self))] |
| 216 | async fn receive_secondary_responses(&mut self, expected_n_responses: usize) -> Result<()> { |
| 217 | for response in self.common.read_commands(expected_n_responses).await? { |
| 218 | match response { |
| 219 | Command::Ok(guid) => { |
| 220 | trace!("Received OK from server" ); |
| 221 | self.set_guid(guid)?; |
| 222 | } |
| 223 | Command::AgreeUnixFD => self.common.set_cap_unix_fd(true), |
| 224 | Command::Error(e) => warn!("UNIX file descriptor passing rejected: {e}" ), |
| 225 | // This also covers "REJECTED", which would mean that the server has rejected the |
| 226 | // authentication challenge response (likely cookie) since it already agreed to the |
| 227 | // mechanism. Theoretically we should be just trying the next auth mechanism but |
| 228 | // this most likely means something is very wrong and we're already too deep into |
| 229 | // the handshake to recover. |
| 230 | cmd => { |
| 231 | return Err(Error::Handshake(format!( |
| 232 | "Unexpected command from server: {cmd}" |
| 233 | ))) |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | Ok(()) |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | #[async_trait ] |
| 243 | impl Handshake for Client { |
| 244 | #[instrument (skip(self))] |
| 245 | async fn perform(mut self) -> Result<Authenticated> { |
| 246 | trace!("Initializing" ); |
| 247 | |
| 248 | #[cfg (any(target_os = "freebsd" , target_os = "dragonfly" ))] |
| 249 | self.send_zero_byte().await?; |
| 250 | |
| 251 | let challenge_response = self.authenticate().await?; |
| 252 | let expected_n_responses = self.send_secondary_commands(challenge_response).await?; |
| 253 | |
| 254 | if expected_n_responses > 0 { |
| 255 | self.receive_secondary_responses(expected_n_responses) |
| 256 | .await?; |
| 257 | } |
| 258 | |
| 259 | trace!("Handshake done" ); |
| 260 | #[cfg (unix)] |
| 261 | let (socket, mut recv_buffer, received_fds, cap_unix_fd, _) = self.common.into_components(); |
| 262 | #[cfg (not(unix))] |
| 263 | let (socket, mut recv_buffer, _, _) = self.common.into_components(); |
| 264 | let (mut read, write) = socket.take(); |
| 265 | |
| 266 | // If we're a bus connection, we need to read the unique name from `Hello` response. |
| 267 | let unique_name = if self.bus { |
| 268 | let unique_name = receive_hello_response(&mut read, &mut recv_buffer).await?; |
| 269 | |
| 270 | Some(unique_name) |
| 271 | } else { |
| 272 | None |
| 273 | }; |
| 274 | |
| 275 | Ok(Authenticated { |
| 276 | socket_write: write, |
| 277 | socket_read: Some(read), |
| 278 | server_guid: self.server_guid.unwrap(), |
| 279 | #[cfg (unix)] |
| 280 | cap_unix_fd, |
| 281 | already_received_bytes: recv_buffer, |
| 282 | #[cfg (unix)] |
| 283 | already_received_fds: received_fds, |
| 284 | unique_name, |
| 285 | }) |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | fn create_hello_method_call() -> Message { |
| 290 | MessageResult::method("/org/freedesktop/DBus" , "Hello" ) |
| 291 | .unwrap() |
| 292 | .destination("org.freedesktop.DBus" ) |
| 293 | .unwrap() |
| 294 | .interface("org.freedesktop.DBus" ) |
| 295 | .unwrap() |
| 296 | .build(&()) |
| 297 | .unwrap() |
| 298 | } |
| 299 | |
| 300 | async fn receive_hello_response( |
| 301 | read: &mut Box<dyn ReadHalf>, |
| 302 | recv_buffer: &mut Vec<u8>, |
| 303 | ) -> Result<OwnedUniqueName> { |
| 304 | use crate::message::Type; |
| 305 | |
| 306 | let reply: Message = readimpl Future |
| 307 | .receive_message( |
| 308 | seq:0, |
| 309 | already_received_bytes:recv_buffer, |
| 310 | #[cfg (unix)] |
| 311 | &mut vec![], |
| 312 | ) |
| 313 | .await?; |
| 314 | match reply.message_type() { |
| 315 | Type::MethodReturn => reply.body().deserialize(), |
| 316 | Type::Error => Err(Error::from(reply)), |
| 317 | m: Type => Err(Error::Handshake(format!("Unexpected messgage ` {m:?}`" ))), |
| 318 | } |
| 319 | } |
| 320 | |