diff --git a/Cargo.toml b/Cargo.toml index fe90c73..0e1d35d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,11 @@ members = [ ] [workspace.dependencies] +futures-channel = "0.3.30" futures-util = "0.3.30" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" time = { version = "0.3", features = ["parsing"] } +tracing = "0.1.40" zbus = { version = "4.2.1" } zvariant = { version = "4.1.0" } diff --git a/bluez/Cargo.toml b/bluez/Cargo.toml index fb48c14..916fac7 100644 --- a/bluez/Cargo.toml +++ b/bluez/Cargo.toml @@ -9,9 +9,14 @@ categories = ["os::linux-apis"] keywords = ["dbus", "bluez", "zbus", "bluetooth"] [dependencies] +futures-channel.workspace = true futures-util.workspace = true +tracing.workspace = true zbus.workspace = true [dev-dependencies] +color-eyre = "0.6.3" +eyre = "0.6.12" pico-args = "0.5.0" tokio = { version = "1", features = ["full"] } +tracing-subscriber = "0.3.18" diff --git a/bluez/examples/bluezagent.rs b/bluez/examples/bluezagent.rs new file mode 100644 index 0000000..2e33273 --- /dev/null +++ b/bluez/examples/bluezagent.rs @@ -0,0 +1,96 @@ +use futures_util::StreamExt; +use tracing_subscriber::prelude::*; +use zbus::zvariant::ObjectPath; + +const AGENT_PATH: &str = "/org/bluez/agent/cosmic"; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + color_eyre::install()?; + + let log_level = std::env::var("RUST_LOG") + .ok() + .and_then(|level| level.parse::().ok()) + .unwrap_or(tracing::Level::DEBUG); + + let log_format = tracing_subscriber::fmt::format() + .pretty() + .without_time() + .with_line_number(true) + .with_file(true) + .with_target(false) + .with_thread_names(true); + + let log_filter = tracing_subscriber::fmt::Layer::default() + .with_writer(std::io::stderr) + .event_format(log_format) + .with_filter(tracing_subscriber::filter::filter_fn(move |metadata| { + metadata.level() <= &log_level + })); + + tracing_subscriber::registry().with(log_filter).init(); + + let system_conn = zbus::Connection::system().await?; + + let (agent, mut receiver) = bluez_zbus::agent1::create(); + + let agent_path = ObjectPath::from_static_str_unchecked(AGENT_PATH); + + tracing::debug!("connecting agent"); + + system_conn.object_server().at(&agent_path, agent).await?; + + tracing::debug!("connecting to bluez agent manager"); + + let bluez = bluez_zbus::agent_manager1::AgentManager1Proxy::new(&system_conn).await?; + + tracing::debug!("registering agent"); + + bluez + .register_agent( + &agent_path, + <&'static str>::from(bluez_zbus::agent1::Capability::DisplayYesNo), + ) + .await?; + + if let Err(why) = bluez.request_default_agent(&agent_path).await { + _ = bluez.unregister_agent(&agent_path).await; + Err(why)?; + } + + tracing::debug!("registered"); + + while let Some(msg) = receiver.next().await { + tracing::debug!(?msg, "message received"); + + match msg { + bluez_zbus::agent1::Message::RequestAuthorization { device, response } => { + _ = response.send(true); + } + + bluez_zbus::agent1::Message::RequestConfirmation { + device, + passkey, + response, + } => { + _ = response.send(true); + } + + bluez_zbus::agent1::Message::RequestPasskey { device, response } => { + _ = response.send(None); + } + + bluez_zbus::agent1::Message::RequestPinCode { device, response } => { + _ = response.send(None); + } + + _ => (), + } + } + + _ = bluez.unregister_agent(&agent_path).await; + + tracing::debug!("exiting"); + + Ok(()) +} diff --git a/bluez/src/agent1.rs b/bluez/src/agent1.rs new file mode 100644 index 0000000..de5b4e4 --- /dev/null +++ b/bluez/src/agent1.rs @@ -0,0 +1,256 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Integrations for creating bluez agents. + +use futures_channel::{mpsc, oneshot}; +use futures_util::SinkExt; +use zbus::zvariant::OwnedObjectPath; + +pub fn create() -> (Agent, mpsc::Receiver) { + let (message_sender, message_receiver) = futures_channel::mpsc::channel(1); + + (Agent { message_sender }, message_receiver) +} + +#[derive(Clone, Copy, Debug)] +pub enum Capability { + DisplayOnly = 0x00, + DisplayYesNo = 0x01, + KeyboardOnly = 0x02, + NoInputNoOutput = 0x03, + KeyboardDisplay = 0x04, +} + +impl From for &'static str { + fn from(capability: Capability) -> &'static str { + match capability { + Capability::DisplayOnly => "DisplayOnly", + Capability::DisplayYesNo => "DisplayYesNo", + Capability::KeyboardOnly => "KeyboardOnly", + Capability::NoInputNoOutput => "NoInputNoOutput", + Capability::KeyboardDisplay => "KeyboardDisplay", + } + } +} + +#[derive(Debug)] +pub enum Message { + AuthorizeService { + device: OwnedObjectPath, + uuid: String, + }, + Cancel, + DisplayPasskey { + device: OwnedObjectPath, + passkey: u32, + entered: u16, + }, + DisplayPinCode { + device: OwnedObjectPath, + pincode: String, + }, + Release, + RequestAuthorization { + device: OwnedObjectPath, + response: oneshot::Sender, + }, + RequestConfirmation { + device: OwnedObjectPath, + passkey: u32, + response: oneshot::Sender, + }, + RequestPasskey { + device: OwnedObjectPath, + response: oneshot::Sender>, + }, + RequestPinCode { + device: OwnedObjectPath, + response: oneshot::Sender>, + }, +} + +pub struct Agent { + pub(self) message_sender: mpsc::Sender, +} + +#[zbus::interface(name = "org.bluez.Agent1")] +impl Agent { + /// This method gets called when the service daemon + /// needs to authorize a connection/service request. + async fn authorize_service( + &mut self, + device: OwnedObjectPath, + uuid: String, + ) -> zbus::fdo::Result<()> { + tracing::debug!(?device, uuid, "authorize_service"); + + Ok(()) + } + + /// This method gets called to indicate that the agent request + /// failed before a reply was returned. + async fn cancel(&mut self) -> zbus::fdo::Result<()> { + tracing::debug!("cancel"); + + _ = self.message_sender.send(Message::Cancel).await; + + Ok(()) + } + + /// This method gets called when the service daemon + /// needs to display a passkey for an authentication. + /// + /// The entered parameter indicates the number of already + /// typed keys on the remote side. + /// + /// An empty reply should be returned. When the passkey + /// needs no longer to be displayed, the Cancel method + /// of the agent will be called. + /// + /// During the pairing process this method might be + /// called multiple times to update the entered value. + /// + /// Note that the passkey will always be a 6-digit number, + /// so the display should be zero-padded at the start if + /// the value contains less than 6 digits. + async fn display_passkey( + &mut self, + device: OwnedObjectPath, + passkey: u32, + entered: u16, + ) -> zbus::fdo::Result<()> { + tracing::debug!(?device, passkey, entered, "display_passkey"); + + Ok(()) + } + + /// This method gets called when the service daemon + /// needs to display a pin code for an authentication. + /// + /// An empty reply should be returned. When the pin code + /// needs no longer to be displayed, the Cancel method + /// of the agent will be called. + /// + /// This is used during the pairing process of keyboards + /// that don't support Bluetooth 2.1 Secure Simple Pairing, + /// in contrast to DisplayPasskey which is used for those + /// that do. + /// + /// This method will only ever be called once since + /// older keyboards do not support typing notification. + /// + /// Note that the PIN will always be a 6-digit number, + /// zero-padded to 6 digits. This is for harmony with + /// the later specification. + async fn display_pin_code( + &mut self, + device: OwnedObjectPath, + pin_code: String, + ) -> zbus::fdo::Result<()> { + tracing::debug!(?device, pin_code, "display_pin_code"); + + Ok(()) + } + + async fn release(&mut self) -> zbus::fdo::Result<()> { + tracing::debug!("release"); + + _ = self.message_sender.send(Message::Release).await; + + Ok(()) + } + + /// This method gets called to request the user to + /// authorize an incoming pairing attempt which + /// would in other circumstances trigger the just-works + /// model, or when the user plugged in a device that + /// implements cable pairing. + /// + /// In the latter case, the + /// device would not be connected to the adapter via + /// Bluetooth yet. + async fn request_authorization(&mut self, device: OwnedObjectPath) -> zbus::fdo::Result<()> { + tracing::debug!(?device, "request_authorization"); + + Ok(()) + } + + /// This method gets called when the service daemon + /// needs to confirm a passkey for an authentication. + /// + /// To confirm the value it should return an empty reply + /// or an error in case the passkey is invalid. + /// + /// Note that the passkey will always be a 6-digit number, + /// so the display should be zero-padded at the start if + /// the value contains less than 6 digits. + async fn request_confirmation( + &mut self, + device: OwnedObjectPath, + passkey: u32, + ) -> zbus::fdo::Result<()> { + tracing::debug!(?device, passkey, "request_confirmation"); + + let (response, response_rx) = oneshot::channel::(); + + _ = self + .message_sender + .send(Message::RequestConfirmation { + device, + passkey, + response, + }) + .await; + + match response_rx.await { + Ok(true) => Ok(()), + Ok(false) => Err(zbus::fdo::Error::Failed("cancelled".to_string())), + Err(why) => Err(zbus::fdo::Error::Failed(why.to_string())), + } + } + + /// This method gets called when the service daemon + /// needs to get the passkey for an authentication. + /// + /// The return value should be a numeric value + /// between 0-999999. + async fn request_passkey(&mut self, device: OwnedObjectPath) -> zbus::fdo::Result { + tracing::debug!(?device, "request_passkey"); + + let (response, response_rx) = oneshot::channel::>(); + + _ = self + .message_sender + .send(Message::RequestPasskey { device, response }) + .await; + + match response_rx.await { + Ok(Some(passkey)) => Ok(passkey), + Ok(None) => Err(zbus::fdo::Error::Failed("cancelled".to_string())), + Err(why) => Err(zbus::fdo::Error::Failed(why.to_string())), + } + } + + /// This method gets called when the service daemon + /// needs to get the passkey for an authentication. + /// + /// The return value should be a string of 1-16 characters + /// length. The string can be alphanumeric. + async fn request_pin_code(&mut self, device: OwnedObjectPath) -> zbus::fdo::Result { + tracing::debug!(?device, "request_pin_code"); + + let (response, response_rx) = oneshot::channel::>(); + + _ = self + .message_sender + .send(Message::RequestPinCode { device, response }) + .await; + + match response_rx.await { + Ok(Some(pin_code)) => Ok(pin_code), + Ok(None) => Err(zbus::fdo::Error::Failed("cancelled".to_string())), + Err(why) => Err(zbus::fdo::Error::Failed(why.to_string())), + } + } +} diff --git a/bluez/src/lib.rs b/bluez/src/lib.rs index e45b165..d3a9845 100644 --- a/bluez/src/lib.rs +++ b/bluez/src/lib.rs @@ -1,8 +1,12 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::collections::HashMap; use futures_util::join; pub mod adapter1; +pub mod agent1; pub mod agent_manager1; pub mod battery1; pub mod device1;