|
| 1 | +use std::{ |
| 2 | + collections::HashMap, |
| 3 | + os::unix::net::UnixStream, |
| 4 | + path::PathBuf, |
| 5 | + sync::Mutex, |
| 6 | +}; |
| 7 | + |
| 8 | +use zbus::{ |
| 9 | + fdo, |
| 10 | + interface, |
| 11 | + object_server::SignalContext, |
| 12 | + zvariant::{ObjectPath, OwnedFd, OwnedObjectPath, OwnedValue}, |
| 13 | + Connection, ObjectServer, |
| 14 | +}; |
| 15 | + |
| 16 | +/// Per-session bookkeeping — currently just the device-type bitmask |
| 17 | +/// `SelectDevices` recorded, kept around for when a real consent UI |
| 18 | +/// needs to show "this app wants keyboard+pointer access" or similar. |
| 19 | +/// Bitmask values match the real portal spec's own convention: |
| 20 | +/// 1 = keyboard, 2 = pointer, 4 = touchscreen. |
| 21 | +#[derive(Default, Clone, Copy)] |
| 22 | +struct Session { |
| 23 | + devices: u32, |
| 24 | +} |
| 25 | + |
| 26 | +struct PortalState { |
| 27 | + eis_socket_path: PathBuf, |
| 28 | + sessions: Mutex<HashMap<String, Session>>, |
| 29 | +} |
| 30 | + |
| 31 | +struct RemoteDesktopPortal { |
| 32 | + state: std::sync::Arc<PortalState>, |
| 33 | +} |
| 34 | + |
| 35 | +/// Exported dynamically, once per `handle` object path, by each of |
| 36 | +/// `create_session`/`select_devices`/`start` — real |
| 37 | +/// `org.freedesktop.impl.portal.Request` shape (an object a caller can |
| 38 | +/// `Close()` to cancel a still-pending request, and which emits exactly |
| 39 | +/// one `Response` signal when the request concludes, positively or |
| 40 | +/// negatively). Stateless itself; the response value is provided by |
| 41 | +/// whichever RemoteDesktopPortal method created it, immediately after |
| 42 | +/// export, not stored on this struct. |
| 43 | +struct RequestObject; |
| 44 | + |
| 45 | +#[interface(name = "org.freedesktop.impl.portal.Request")] |
| 46 | +impl RequestObject { |
| 47 | + /// Real requests would stop whatever's pending and emit a |
| 48 | + /// cancelled `Response` here — since every request in this |
| 49 | + /// implementation has already been auto-approved and resolved by |
| 50 | + /// the time a caller could possibly call `Close`, this is a no-op |
| 51 | + /// stub that exists for interface-shape completeness. |
| 52 | + async fn close(&self) {} |
| 53 | + |
| 54 | + #[zbus(signal)] |
| 55 | + async fn response( |
| 56 | + signal_ctxt: &SignalContext<'_>, |
| 57 | + response: u32, |
| 58 | + results: HashMap<String, OwnedValue>, |
| 59 | + ) -> zbus::Result<()>; |
| 60 | +} |
| 61 | + |
| 62 | +/// Exports a `RequestObject` at `handle` and immediately emits a |
| 63 | +/// success `Response(0, results)` signal on it — the "auto-approve" |
| 64 | +/// simplification this module's doc is explicit about, factored out |
| 65 | +/// since all three of `create_session`/`select_devices`/`start` do |
| 66 | +/// exactly this same sequence. |
| 67 | +async fn respond_success( |
| 68 | + object_server: &ObjectServer, |
| 69 | + handle: &ObjectPath<'_>, |
| 70 | + results: HashMap<String, OwnedValue>, |
| 71 | +) -> fdo::Result<()> { |
| 72 | + let owned_handle = OwnedObjectPath::from(handle.to_owned()); |
| 73 | + object_server |
| 74 | + .at(&owned_handle, RequestObject) |
| 75 | + .await |
| 76 | + .map_err(|e| fdo::Error::Failed(format!("failed to export Request object: {e}")))?; |
| 77 | + let iface_ref = object_server |
| 78 | + .interface::<_, RequestObject>(&owned_handle) |
| 79 | + .await |
| 80 | + .map_err(|e| fdo::Error::Failed(format!("failed to look up just-exported Request object: {e}")))?; |
| 81 | + RequestObject::response(iface_ref.signal_context(), 0, results) |
| 82 | + .await |
| 83 | + .map_err(|e| fdo::Error::Failed(format!("failed to emit Response signal: {e}")))?; |
| 84 | + Ok(()) |
| 85 | +} |
| 86 | + |
| 87 | +#[interface(name = "org.freedesktop.impl.portal.RemoteDesktop")] |
| 88 | +impl RemoteDesktopPortal { |
| 89 | + async fn create_session( |
| 90 | + &self, |
| 91 | + handle: ObjectPath<'_>, |
| 92 | + session_handle: ObjectPath<'_>, |
| 93 | + _app_id: String, |
| 94 | + _options: HashMap<String, OwnedValue>, |
| 95 | + #[zbus(object_server)] object_server: &ObjectServer, |
| 96 | + ) -> fdo::Result<()> { |
| 97 | + self.state |
| 98 | + .sessions |
| 99 | + .lock() |
| 100 | + .unwrap() |
| 101 | + .insert(session_handle.to_string(), Session::default()); |
| 102 | + respond_success(object_server, &handle, HashMap::new()).await |
| 103 | + } |
| 104 | + |
| 105 | + async fn select_devices( |
| 106 | + &self, |
| 107 | + handle: ObjectPath<'_>, |
| 108 | + session_handle: ObjectPath<'_>, |
| 109 | + _app_id: String, |
| 110 | + options: HashMap<String, OwnedValue>, |
| 111 | + #[zbus(object_server)] object_server: &ObjectServer, |
| 112 | + ) -> fdo::Result<()> { |
| 113 | + // Real callers pass the requested device bitmask under the |
| 114 | + // "types" option key (u32) — this compositor doesn't currently |
| 115 | + // *restrict* what ConnectToEIS hands back based on it (the one |
| 116 | + // EIS socket serves pointer+keyboard+touch uniformly regardless |
| 117 | + // — see input_emulation::init/render/mod.rs's add_touch calls), |
| 118 | + // just records it for whenever a real consent UI wants to show |
| 119 | + // what was asked for. |
| 120 | + let devices = options |
| 121 | + .get("types") |
| 122 | + .and_then(|v| u32::try_from(v.clone()).ok()) |
| 123 | + .unwrap_or(0); |
| 124 | + if let Some(session) = self.state.sessions.lock().unwrap().get_mut(&session_handle.to_string()) { |
| 125 | + session.devices = devices; |
| 126 | + } |
| 127 | + respond_success(object_server, &handle, HashMap::new()).await |
| 128 | + } |
| 129 | + |
| 130 | + async fn start( |
| 131 | + &self, |
| 132 | + handle: ObjectPath<'_>, |
| 133 | + _session_handle: ObjectPath<'_>, |
| 134 | + _app_id: String, |
| 135 | + _parent_window: String, |
| 136 | + _options: HashMap<String, OwnedValue>, |
| 137 | + #[zbus(object_server)] object_server: &ObjectServer, |
| 138 | + ) -> fdo::Result<()> { |
| 139 | + respond_success(object_server, &handle, HashMap::new()).await |
| 140 | + } |
| 141 | + |
| 142 | + /// The one method with a genuinely immediate, synchronous result — |
| 143 | + /// real portal spec has this one return the fd directly rather than |
| 144 | + /// through the Request/Response pattern the other three use (there's |
| 145 | + /// nothing left to negotiate by this point; the session was already |
| 146 | + /// approved in `start`). |
| 147 | + async fn connect_to_eis( |
| 148 | + &self, |
| 149 | + _session_handle: ObjectPath<'_>, |
| 150 | + _app_id: String, |
| 151 | + _options: HashMap<String, OwnedValue>, |
| 152 | + ) -> fdo::Result<OwnedFd> { |
| 153 | + let stream = UnixStream::connect(&self.state.eis_socket_path).map_err(|e| { |
| 154 | + fdo::Error::Failed(format!( |
| 155 | + "failed to connect to internal EIS socket at {}: {e}", |
| 156 | + self.state.eis_socket_path.display() |
| 157 | + )) |
| 158 | + })?; |
| 159 | + Ok(OwnedFd::from(std::os::fd::OwnedFd::from(stream))) |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +pub fn init(eis_socket_path: PathBuf) { |
| 164 | + std::thread::spawn(move || { |
| 165 | + let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() { |
| 166 | + Ok(rt) => rt, |
| 167 | + Err(e) => { |
| 168 | + tracing::warn!("portal: failed to start tokio runtime, RemoteDesktop portal disabled: {e}"); |
| 169 | + return; |
| 170 | + } |
| 171 | + }; |
| 172 | + rt.block_on(async move { |
| 173 | + let state = std::sync::Arc::new(PortalState { |
| 174 | + eis_socket_path, |
| 175 | + sessions: Mutex::new(HashMap::new()), |
| 176 | + }); |
| 177 | + let portal = RemoteDesktopPortal { state }; |
| 178 | + |
| 179 | + let connection: Connection = match Connection::session().await { |
| 180 | + Ok(c) => c, |
| 181 | + Err(e) => { |
| 182 | + tracing::warn!("portal: no D-Bus session bus available, RemoteDesktop portal disabled: {e}"); |
| 183 | + return; |
| 184 | + } |
| 185 | + }; |
| 186 | + if let Err(e) = connection.object_server().at("/org/freedesktop/portal/desktop", portal).await { |
| 187 | + tracing::warn!("portal: failed to export RemoteDesktop interface: {e}"); |
| 188 | + return; |
| 189 | + } |
| 190 | + if let Err(e) = connection.request_name("org.freedesktop.impl.portal.desktop.blue").await { |
| 191 | + tracing::warn!("portal: failed to claim D-Bus service name (another portal backend already running?): {e}"); |
| 192 | + return; |
| 193 | + } |
| 194 | + |
| 195 | + tracing::info!( |
| 196 | + "RemoteDesktop D-Bus portal backend registered at \ |
| 197 | + org.freedesktop.impl.portal.desktop.blue — see portal/mod.rs's \ |
| 198 | + module doc for the real, load-bearing security caveat (no \ |
| 199 | + consent UI, auto-approves every request) before relying on \ |
| 200 | + this for anything real" |
| 201 | + ); |
| 202 | + std::future::pending::<()>().await; |
| 203 | + }); |
| 204 | + }); |
| 205 | +} |
0 commit comments