summary refs log tree commit diff
path: root/tools/xdg-desktop-portal-spectrum-host/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tools/xdg-desktop-portal-spectrum-host/src/main.rs')
-rw-r--r--tools/xdg-desktop-portal-spectrum-host/src/main.rs272
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();
+        }
+    }))
+}