diff options
Diffstat (limited to 'tools/xdg-desktop-portal-spectrum-host/src/main.rs')
-rw-r--r-- | tools/xdg-desktop-portal-spectrum-host/src/main.rs | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/tools/xdg-desktop-portal-spectrum-host/src/main.rs b/tools/xdg-desktop-portal-spectrum-host/src/main.rs new file mode 100644 index 0000000..c91a5d4 --- /dev/null +++ b/tools/xdg-desktop-portal-spectrum-host/src/main.rs @@ -0,0 +1,272 @@ +mod guest_dbus; +mod host_bus; + +use std::cmp::max; +use std::env::args_os; +use std::ffi::{CString, OsStr, OsString}; +use std::fs::File; +use std::io::{self, ErrorKind}; +use std::os::linux::net::SocketAddrExt; +use std::os::unix::net::{SocketAddr, UnixListener, UnixStream}; +use std::os::unix::prelude::*; +use std::path::{Path, PathBuf}; +use std::ptr; +use std::process::{exit, Command}; +use std::slice; +use std::sync::OnceLock; + +use async_executor::Executor; +use async_io::Async; +use futures_lite::prelude::*; +use futures_lite::stream::StreamExt; +use libc::{mount, MS_BIND, MS_REC}; +use zbus::{ConnectionBuilder, Guid, MessageStream}; + +use guest_dbus::{FileChooserImpl, FileChooserProxy}; + +static VSOCK_UNIX_PATH: OnceLock<PathBuf> = OnceLock::new(); +static FILE_CHOOSER_PROXY: OnceLock<FileChooserProxy> = OnceLock::new(); + +fn share_file(source_path: PathBuf, writable: bool) { + assert!(source_path.is_file()); + assert!(writable); + + let fs_root_dir = std::env::var_os("XDG_DESKTOP_PORTAL_SPECTRUM_HOST_FS_ROOT").unwrap(); + let name = source_path.file_name().unwrap(); + + let dest_path = Path::new(&fs_root_dir).join(name); + + File::create(&dest_path).unwrap(); + + let c_source_path = CString::new(source_path.into_os_string().into_vec()).unwrap(); + let c_dest_path = CString::new(dest_path.into_os_string().into_vec()).unwrap(); + + unsafe { + if mount(c_source_path.as_ptr(), c_dest_path.as_ptr(), ptr::null(), MS_BIND|MS_REC, ptr::null()) == -1 { + panic!("{}", io::Error::last_os_error()); + } + } +} + +async fn negotiate_version(conn: &mut Async<UnixStream>) -> Result<(), String> { + let mut num_versions = 0; + conn.read_exact(slice::from_mut(&mut num_versions)) + .await + .map_err(|e| format!("reading number of versions supported by client: {e}"))?; + + let mut client_versions = vec![0; num_versions.into()]; + conn.read_exact(&mut client_versions) + .await + .map_err(|e| format!("reading versions supported by client: {e}"))?; + + if !client_versions.contains(&1) { + let msg = format!( + "no supported protocol versions offered by client: {:?}", + client_versions + ); + return Err(msg); + } + + conn.write_all(&[1]) + .await + .map_err(|e| format!("communicating chosen protocol version to client: {e}"))?; + + Ok(()) +} + +async fn receive_port(conn: &mut Async<UnixStream>) -> Result<u32, String> { + negotiate_version(conn).await?; + + let mut port = [0; 4]; + conn.read_exact(&mut port) + .await + .map_err(|e| format!("reading port: {e}"))?; + + if let Ok(1) = conn.read(&mut [0]).await { + return Err("unexpected data following port".to_string()); + } + + Ok(u32::from_be_bytes(port)) +} + +async fn connect_to_guest(port: u32) -> Result<Async<UnixStream>, String> { + let vsock_unix_path = VSOCK_UNIX_PATH.get().unwrap(); + let mut vsock = Async::<UnixStream>::connect(vsock_unix_path) + .await + .map_err(|e| format!("connecting to {:?}: {e}", vsock_unix_path))?; + for part in [b"CONNECT ", port.to_string().as_bytes(), b"\n"] { + vsock + .write_all(part) + .await + .map_err(|e| format!("writing to VSOCK UNIX socket: {e}"))?; + } + + // TODO: this shouldn't be an unwrap, because it could actually happen. + // TODO: MSG_PEEK? + let mut response = [0; 14]; // Length of "OK [u32::MAX]\n" is 14. + let min_response_len = "OK 0\n".len(); + let mut response_len = 0usize; + + while response_len.checked_sub(1).and_then(|i| response.get(i)) != Some(&b'\n') + && response_len < response.len() + { + let min_remaining = min_response_len.saturating_sub(response_len); + let end = max(min_remaining, response_len + 1); + let dest = response.get_mut(response_len..end).unwrap(); + response_len += dest.len(); + vsock + .read_exact(dest) + .await + .map_err(|e| format!("reading VSOCK connection response: {e}"))?; + } + + if !response.starts_with(b"OK ") { + return Err(format!( + "unexpected response from Cloud Hypervisor VSOCK handshake: {:#x?}", + response + )); + } + + Ok(vsock) +} + +async fn run_guest_connection(mut conn: Async<UnixStream>) -> Result<(), String> { + let port = receive_port(&mut conn).await?; + let vsock = connect_to_guest(port).await?; + + let guest_conn = ConnectionBuilder::socket(vsock) + .name("org.freedesktop.impl.portal.desktop.spectrum") + .unwrap() + .serve_at("/org/freedesktop/portal/desktop", FileChooserImpl) + .unwrap() + .build() + .await + .map_err(|e| format!("setting up connection to guest bus: {e}"))?; + + eprintln!("Created org.freedesktop.impl.portal.desktop.spectrum.host on guest bus"); + + let mut guest_messages = MessageStream::from(guest_conn); + loop { + match guest_messages.try_next().await { + Ok(_) => (), + Err(zbus::Error::InputOutput(e)) if e.kind() == ErrorKind::BrokenPipe => break Ok(()), + Err(e) => return Err(format!("communicating with guest bus: {e}")), + } + } +} + +fn read_argv() { + let mut args = args_os(); + args.next(); + + if args.next().is_some() { + eprintln!("too many arguments"); + exit(1); + } +} + +async fn spawn_portal_impl() -> Result<(), Box<dyn std::error::Error>> { + let impl_conn_guid = Guid::generate(); + let addr = SocketAddr::from_abstract_name(impl_conn_guid.as_bytes())?; + let impl_listener = Async::new(UnixListener::bind_addr(&addr)?)?; + + let bus_addr = format!("unix:abstract={impl_conn_guid}"); + Command::new("xdg-desktop-portal-gtk") + .env("DBUS_SESSION_BUS_ADDRESS", &bus_addr) + .spawn()?; + + let (impl_conn, _) = impl_listener.accept().await?; + + let host_bus = host_bus::Bus::new(); + let name_owned = host_bus.name_owned(); + + let impl_conn = ConnectionBuilder::socket(impl_conn) + .p2p() + .server(Guid::generate()) + .unwrap() + .serve_at("/org/freedesktop/DBus", host_bus)? + .build() + .await?; + + name_owned.await; + + FILE_CHOOSER_PROXY + .set(FileChooserProxy::new(&impl_conn, "org.freedesktop.impl.portal.desktop.gtk").await?) + .unwrap(); + + Ok(()) +} + +fn listening_vsock_path(connection: &UnixListener) -> io::Result<PathBuf> { + let Some(mut listening_addr) = connection + .local_addr()? + .as_pathname() + .map(Path::as_os_str) + .map(OsStr::to_os_string) + .map(OsString::into_vec) + else { + eprintln!("stdin is not listening on a path"); + exit(1); + }; + + let mut i = listening_addr.len() - 1; + + loop { + match (i != 0).then_some(listening_addr[i]) { + Some(b'0'..=b'9') => { + i -= 1; + } + Some(b'_') => { + break; + } + _ => { + let os_string = OsString::from_vec(listening_addr); + eprintln!("can't infer listening VSOCK path from {:?}", os_string); + exit(1); + } + } + } + + listening_addr.truncate(i); + Ok(OsString::from_vec(listening_addr).into()) +} + +// TODO: let's not have main return a Result. +fn main() -> Result<(), Box<dyn std::error::Error>> { + read_argv(); + + let ex = Executor::new(); + + async_io::block_on(ex.run(async { + // SAFETY: safe because we won't use fd 0 anywhere else. + let stdin = Async::try_from(unsafe { UnixListener::from_raw_fd(0) })?; + + VSOCK_UNIX_PATH + .set(listening_vsock_path(stdin.get_ref())?) + .unwrap(); + + let mut portal_impl_spawned = false; + + loop { + let (conn, _) = match stdin.accept().await { + Ok(conn) => conn, + Err(e) => { + eprintln!("accepting connection from guest: {e}"); + continue; + } + }; + + if !portal_impl_spawned { + spawn_portal_impl().await?; + portal_impl_spawned = true; + } + + ex.spawn(async move { + if let Err(e) = run_guest_connection(conn).await { + eprintln!("guest connection error: {e}"); + } + }) + .detach(); + } + })) +} |