Skip to content

Add a portal implementation #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ jobs:
- name: Build client (tokio / OpenSSL)
run: |
cargo build --manifest-path ./client/Cargo.toml --no-default-features --features tokio --features openssl_crypto

- name: Build CLI
run: |
cargo build --manifest-path ./cli/Cargo.toml

- name: Build Portal
run: |
cargo build --manifest-path ./portal/Cargo.toml

- name: Test (native)
run: |
cargo test --manifest-path ./client/Cargo.toml --no-default-features --features async-std --features native_crypto
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
/portal/target
Cargo.lock
cli/target
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ resolver = "2"
members = [
"client",
"cli",
"portal",
]
17 changes: 17 additions & 0 deletions portal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "oo7-portal"
version = "0.1.0"
edition = "2021"

[dependencies]
futures-channel = "0.3"
futures-util = "0.3"
oo7 = { path = "../client" }
ring = "0.17.5"
secrecy = { version = "0.8", features = ["alloc"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.17", features = ["io-util", "net", "macros", "rt-multi-thread"] }
tracing = "0.1"
tracing-subscriber = "0.3"
zbus = "3.5.0"
zeroize = { version = "1", features = ["zeroize_derive"] }
4 changes: 4 additions & 0 deletions portal/data/oo7-portal.portal
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.desktop.oo7
Interfaces=org.freedesktop.impl.portal.Secret
UseIn=gnome
3 changes: 3 additions & 0 deletions portal/data/oo7-portal.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.impl.portal.Secret
Exec=@bindir@/oo7-portal
35 changes: 35 additions & 0 deletions portal/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use oo7::zbus;

#[derive(zbus::DBusError, Debug)]
pub enum Error {
Owned(String),
}

impl From<zbus::fdo::Error> for Error {
fn from(err: zbus::fdo::Error) -> Self {
Self::Owned(err.to_string())
}
}
impl From<zbus::Error> for Error {
fn from(err: zbus::Error) -> Self {
Self::Owned(err.to_string())
}
}

impl From<oo7::dbus::Error> for Error {
fn from(err: oo7::dbus::Error) -> Self {
Self::Owned(err.to_string())
}
}

impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Self::Owned(err.to_string())
}
}

impl From<ring::error::Unspecified> for Error {
fn from(err: ring::error::Unspecified) -> Self {
Self::Owned(err.to_string())
}
}
153 changes: 153 additions & 0 deletions portal/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
mod error;
mod request;

use std::{
collections::HashMap,
future::pending,
os::{
fd::{AsRawFd, FromRawFd},
unix::net::UnixStream,
},
};

use futures_util::FutureExt;
use oo7::{
dbus::Service,
zbus::{self, dbus_interface, zvariant},
};
use ring::rand::SecureRandom;
use tokio::io::AsyncWriteExt;
use zvariant::OwnedObjectPath;

use crate::{
error::Error,
request::{Request, ResponseType},
};

const PORTAL_VERSION: u32 = 1;
const PORTAL_SECRET_SIZE: usize = 64;
const PORTAL_NAME: &str = "org.freedesktop.impl.portal.desktop.oo7";
const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";

struct Secret;

#[dbus_interface(name = "org.freedesktop.impl.portal.Secret")]
impl Secret {
#[dbus_interface(property, name = "version")]
fn version(&self) -> u32 {
PORTAL_VERSION
}

#[dbus_interface(out_args("response", "results"))]
async fn retrieve_secret(
&self,
#[zbus(object_server)] object_server: &zbus::ObjectServer,
handle: OwnedObjectPath,
app_id: &str,
fd: zvariant::Fd,
options: HashMap<&str, zvariant::Value<'_>>,
) -> Result<(ResponseType, HashMap<&str, zvariant::OwnedValue>), Error> {
tracing::debug!("Request from app: {app_id} with options: {options:?}");

let (sender, receiver) = futures_channel::oneshot::channel();
let request = Request::new(&handle, sender);
object_server.at(&handle, request).await?;

let fut_1 = async move {
let res = match send_secret_to_app(app_id, fd).await {
Ok(_) => ResponseType::Success,
Err(err) => {
tracing::error!("could not retrieve secret: {err}");
ResponseType::Other
}
};

// We do not accept Close request anymore here.
tracing::debug!("Request {handle} handled");
object_server.remove::<Request, _>(&handle).await.unwrap();

Ok((res, HashMap::new()))
};

let fut_2 = async move {
receiver.await.unwrap();
Ok((ResponseType::Cancelled, HashMap::new()))
};

let t1 = fut_1.fuse();
let t2 = fut_2.fuse();

futures_util::pin_mut!(t1, t2);

futures_util::select! {
fut_1_res = t1 => fut_1_res,
fut_2_res = t2 => fut_2_res,
}
}
}

fn generate_secret() -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
let mut secret = [0; PORTAL_SECRET_SIZE];
let rand = ring::rand::SystemRandom::new();
rand.fill(&mut secret)?;
Ok(zeroize::Zeroizing::new(secret.to_vec()))
}

/// Generates, stores and send the secret back to the fd stream
async fn send_secret_to_app(app_id: &str, fd: zvariant::Fd) -> Result<(), Error> {
let service = Service::new().await?;
let collection = match service.default_collection().await {
Err(oo7::dbus::Error::NotFound(_)) => {
service
.create_collection("Default", Some(oo7::dbus::DEFAULT_COLLECTION))
.await
}
e => e,
}?;
let attributes = HashMap::from([("app_id", app_id)]);

let secret = if let Some(item) = collection.search_items(&attributes).await?.first() {
item.secret().await?
} else {
tracing::debug!("Could not find secret for {app_id}, creating one");
let secret = generate_secret()?;
collection
.create_item(
&format!("Secret Portal token for {app_id}"),
&attributes,
&secret,
true,
// TODO Find a better one.
"text/plain",
)
.await?;

secret
};

// Write the secret to the FD.
let raw_fd = fd.as_raw_fd();
let mut stream = unsafe { tokio::net::UnixStream::from_std(UnixStream::from_raw_fd(raw_fd)) }?;
stream.write_all(&secret).await?;

Ok(())
}

#[tokio::main]
async fn main() -> Result<(), zbus::Error> {
tracing_subscriber::fmt::init();

let backend = Secret;
let cnx = zbus::ConnectionBuilder::session()?
.serve_at(PORTAL_PATH, backend)?
.build()
.await?;
// NOTE For debugging.
let flags = zbus::fdo::RequestNameFlags::ReplaceExisting
| zbus::fdo::RequestNameFlags::AllowReplacement;
cnx.request_name_with_flags(PORTAL_NAME, flags).await?;

loop {
pending::<()>().await;
}
}
52 changes: 52 additions & 0 deletions portal/src/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use std::sync::Mutex;

use oo7::zbus::{
self, dbus_interface,
zvariant::{self, ObjectPath, Type},
};
use serde::Serialize;

#[derive(Serialize, PartialEq, Eq, Debug, Type)]
pub enum ResponseType {
Success = 0,
Cancelled = 1,
Other = 2,
}

pub struct Request {
handle_path: zvariant::ObjectPath<'static>,
sender: Mutex<Option<futures_channel::oneshot::Sender<()>>>,
}

impl Request {
pub fn new(
handle_path: &ObjectPath<'static>,
sender: futures_channel::oneshot::Sender<()>,
) -> Self {
tracing::debug!("Request `{:?}` exported", handle_path.as_str());
Self {
handle_path: handle_path.clone(),
sender: Mutex::new(Some(sender)),
}
}
}

#[dbus_interface(name = "org.freedesktop.impl.portal.Request")]
impl Request {
async fn close(
&self,
#[zbus(object_server)] server: &zbus::ObjectServer,
) -> zbus::fdo::Result<()> {
tracing::debug!("Request `{}` closed", self.handle_path);
server.remove::<Self, _>(&self.handle_path).await?;

if let Ok(mut guard) = self.sender.lock() {
if let Some(sender) = (*guard).take() {
// This will Err out if the receiver has been dropped.
let _ = sender.send(());
}
}

Ok(())
}
}