Compare commits

...

13 Commits

Author SHA1 Message Date
Marc-André Lureau c863a57e4d Update Cargo.lock
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 14:44:15 +04:00
Marc-André Lureau 9cd1267c55 qemu-rdp: add cursor shape support
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 14:44:15 +04:00
Marc-André Lureau 1e0aa2b772 qemu-rdp: add basic clipboard
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 14:44:15 +04:00
Marc-André Lureau 192ed89593 qemu-rdp: add SSLKEYLOGFILE support
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 14:26:56 +04:00
Marc-André Lureau 0a68e1e08d qemu-rdp: some ConsoleListner improvements
- use pixman-sys
- reuse self.update() for scanout
- save a RDP DesktopSize for future resize usage
- add some tracing

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 14:03:08 +04:00
Marc-André Lureau 187f680599 qemu-rdp: simplify logging
Drop the --log-file argument, simply log to std.

Adjust environment name to follow IRONRDP_LOG style.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 13:01:37 +04:00
Marc-André Lureau 726c893298 qemu-display: start a examples/server
This is meant for testing.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:57:24 +04:00
Marc-André Lureau 4395b41fd7 qemu-display: some extra derive
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:56:32 +04:00
Marc-André Lureau fd67aa0277 qemu-display: bump deps
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:55:56 +04:00
Marc-André Lureau 79fa942b7a qemu-rdw: fix a clippy warning
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:14:51 +04:00
Bilal Elmoussaoui 504611a853 qemu-display: Add a MultiTouch interface implementation 2024-04-10 12:14:51 +04:00
Marc-André Lureau 912d39dc20 chore: commit Cargo.lock
This allows to track our working dependencies, and is now a recommended practice, even for lib

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:14:51 +04:00
Marc-André Lureau e03f2d80a6 qemu-rdp: fix compilation with current IronRDP
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
2024-04-10 12:14:51 +04:00
18 changed files with 6164 additions and 128 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
/target
Cargo.lock
.DS_Store
.idea
*.log

5229
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,11 @@ default-members = ["qemu-rdw"]
resolver = "2"
[workspace.dependencies]
tracing = "0.1"
zbus = { version = "4", features = ["p2p"] }
pixman-sys = "0.1"
qemu-display = { path = "qemu-display", version = "0.1" }
keycodemap = { path = "keycodemap", version = "0.1" }
zbus = { version = "4", features = ["p2p"] }
[patch.crates-io]
vnc = { git = "https://github.com/elmarco/rust-vnc", branch = "server" }

View File

@ -10,10 +10,10 @@ edition = "2018"
qmp = ["dep:qapi", "dep:base64"]
[dependencies]
cfg-if = "1.0"
log = "0.4"
derivative = "2.2.0"
tracing.workspace = true
zbus.workspace = true
cfg-if = "1.0"
derivative = "2.2.0"
zvariant = { version = "4", features = ["serde_bytes"] }
libc = "0.2.86"
enumflags2 = { version = "0.7", features = ["serde"] }
@ -24,9 +24,9 @@ futures-util = { version = "0.3.8", features = ["async-await-macro"] }
once_cell = "1.5"
futures = "0.3.13"
usbredirhost = "0.0.1"
async-broadcast = "0.3.3"
async-trait = "0.1.48"
async-lock = "2.3.0"
async-broadcast = "0.7"
async-trait = "0.1.77"
async-lock = "3.3.0"
qapi = { version = "0.9.0", features = ["qmp"], optional = true }
base64 = { version = "0.13", optional = true }
@ -38,6 +38,12 @@ windows = { version = "0.43.0", features = ["Win32_Networking_WinSock", "Win32_F
async-std = { version = "1.12.0", features = ["attributes"] }
tracing-subscriber = { version = "0.3.11", features = ["env-filter" , "fmt"], default-features = false }
[dev-dependencies]
pixman-sys.workspace = true
async-std = { version = "1.12", features = ["attributes"] }
tracing-subscriber = "0.3"
rand = "0.8"
[[example]]
name = 'win32-test'
required-features = ["qmp"]

View File

@ -0,0 +1,169 @@
use std::{
cmp,
os::{fd::AsFd, unix::net::UnixStream},
};
use serde_bytes::ByteBuf;
use zbus::connection;
const WIDTH: u32 = 1024;
const HEIGHT: u32 = 768;
struct VM;
#[zbus::interface(name = "org.qemu.Display1.VM")]
impl VM {
#[zbus(property, name = "ConsoleIDs")]
async fn console_ids(&self) -> Vec<u8> {
vec![0]
}
#[zbus(property)]
async fn interfaces(&self) -> Vec<String> {
vec![]
}
#[zbus(property, name = "UUID")]
async fn uuid(&self) -> String {
"00000000-0000-0000-0000-000000000000".into()
}
#[zbus(property)]
async fn name(&self) -> &str {
"VM Name"
}
}
#[zbus::proxy(
interface = "org.qemu.Display1.Listener",
default_path = "/org/qemu/Display1/Listener",
default_service = "org.qemu"
)]
trait Listener {
fn scanout(
&self,
width: u32,
height: u32,
stride: u32,
format: u32,
data: serde_bytes::ByteBuf,
) -> zbus::Result<()>;
fn update(
&self,
x: i32,
y: i32,
w: i32,
h: i32,
stride: u32,
format: u32,
data: serde_bytes::ByteBuf,
) -> zbus::Result<()>;
}
struct Console {
task: Option<zbus::Task<()>>,
}
#[zbus::interface(name = "org.qemu.Display1.Console")]
impl Console {
async fn register_listener(
&mut self,
#[zbus(connection)] conn: &zbus::Connection,
listener: zbus::zvariant::Fd<'_>,
) {
let fd = listener.as_fd().try_clone_to_owned().unwrap();
let task = conn.executor().spawn(
async move {
let stream = UnixStream::from(fd);
let conn = connection::Builder::unix_stream(stream)
.server(zbus::Guid::generate())
.unwrap()
.p2p()
.build()
.await
.unwrap();
let listener = ListenerProxy::new(&conn).await.unwrap();
let format = pixman_sys::pixman_format_code_t_PIXMAN_x8r8g8b8;
let data = vec![0u8; WIDTH as usize * HEIGHT as usize * 4];
listener
.scanout(WIDTH, HEIGHT, WIDTH * 4, format, ByteBuf::from(data))
.await
.unwrap();
let mut x = 0i32;
let mut y = 0i32;
loop {
let w = cmp::min(256, WIDTH - x as u32) as _;
let h = cmp::min(256, HEIGHT - y as u32) as _;
let data: Vec<u8> = (0..w * h * 4).map(|_| rand::random()).collect();
listener
.update(x, y, w, h, w as u32 * 4, format, ByteBuf::from(data))
.await
.unwrap();
x += 1;
if x >= WIDTH as _ {
x = 0;
y += 1;
}
// async_std::task::sleep(core::time::Duration::from_millis(1)).await;
}
},
"display",
);
self.task = Some(task);
}
#[zbus(name = "SetUIInfo")]
fn set_ui_info(
&self,
_width_mm: u16,
_height_mm: u16,
_xoff: i32,
_yoff: i32,
width: u32,
height: u32,
) {
tracing::warn!(%width, %height, "set-ui");
}
#[zbus(property)]
fn label(&self) -> String {
"label".into()
}
#[zbus(property)]
fn head(&self) -> u32 {
0
}
#[zbus(property)]
fn type_(&self) -> String {
"Graphic".into()
}
#[zbus(property)]
fn width(&self) -> u32 {
1024
}
#[zbus(property)]
fn height(&self) -> u32 {
768
}
}
#[async_std::main]
async fn main() -> zbus::Result<()> {
tracing_subscriber::fmt::init();
let _connection = connection::Builder::session()?
.name("org.qemu")?
.serve_at("/org/qemu/Display1/VM", VM)?
.serve_at("/org/qemu/Display1/Console_0", Console { task: None })?
.build()
.await?;
loop {
std::future::pending::<()>().await;
}
}

View File

@ -86,7 +86,7 @@ impl<H: ClipboardHandler> ClipboardListener<H> {
}
#[derive(derivative::Derivative)]
#[derivative(Debug)]
#[derivative(Clone, Debug)]
pub struct Clipboard {
#[derivative(Debug = "ignore")]
pub proxy: ClipboardProxy<'static>,

View File

@ -9,7 +9,10 @@ use uds_windows::UnixStream;
use zbus::zvariant::Fd;
use zbus::{zvariant::ObjectPath, Connection};
use crate::{util, ConsoleListener, ConsoleListenerHandler, KeyboardProxy, MouseProxy, Result};
use crate::{
util, ConsoleListener, ConsoleListenerHandler, KeyboardProxy, MouseProxy, MultiTouchProxy,
Result,
};
#[cfg(windows)]
use crate::{
ConsoleListenerD3d11, ConsoleListenerD3d11Handler, ConsoleListenerMap,
@ -58,6 +61,8 @@ pub struct Console {
pub keyboard: KeyboardProxy<'static>,
#[derivative(Debug = "ignore")]
pub mouse: MouseProxy<'static>,
#[derivative(Debug = "ignore")]
pub multi_touch: MultiTouchProxy<'static>,
listener: RwLock<Option<Connection>>,
#[cfg(windows)]
peer_pid: u32,
@ -72,10 +77,15 @@ impl Console {
.build()
.await?;
let mouse = MouseProxy::builder(conn).path(&obj_path)?.build().await?;
let multi_touch = MultiTouchProxy::builder(conn)
.path(&obj_path)?
.build()
.await?;
Ok(Self {
proxy,
keyboard,
mouse,
multi_touch,
listener: RwLock::new(None),
#[cfg(windows)]
peer_pid,
@ -94,6 +104,7 @@ impl Console {
Ok(self.proxy.height().await?)
}
#[tracing::instrument(skip(self, handler))]
pub async fn register_listener<H: ConsoleListenerHandler>(&self, handler: H) -> Result<()> {
let (p0, p1) = UnixStream::pair()?;
let p0 = util::prepare_uds_pass(

View File

@ -36,6 +36,9 @@ pub use mouse::*;
mod display;
pub use display::*;
mod multi_touch;
pub use multi_touch::*;
#[cfg(unix)]
mod usbredir;
#[cfg(unix)]

View File

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
use zbus::zvariant::Type;
#[repr(u32)]
#[derive(Type, Debug, PartialEq, Copy, Clone, Eq, Serialize, Deserialize)]
pub enum TouchEventKind {
Begin = 0,
Update = 1,
End = 2,
Cancel = 3,
}
#[zbus::proxy(
default_service = "org.qemu",
interface = "org.qemu.Display1.MultiTouch"
)]
pub trait MultiTouch {
fn send_event(&self, kind: TouchEventKind, num_slot: u64, x: f64, y: f64) -> zbus::Result<()>;
#[dbus_proxy(property)]
fn max_slots(&self) -> zbus::Result<i32>;
}

View File

@ -8,10 +8,11 @@ edition = "2021"
[dependencies]
qemu-display = { path = "../qemu-display" }
tracing = "0.1.37"
tracing.workspace = true
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
keycodemap = { path = "../keycodemap" }
bytes = "1.4"
pixman-sys = "0.1.0"
rustls = { version = "0.21" }
rustls-pemfile = "1.0"
tokio = { version = "1.28", features = ["full"] }
@ -19,4 +20,4 @@ tokio-rustls = "0.24"
anyhow = "1.0"
clap = { version = "4.2", features = ["derive", "cargo"] }
async-trait = "0.1"
ironrdp = { git = "https://github.com/Devolutions/IronRDP", features = ["server"] }
ironrdp = { git = "https://github.com/Devolutions/IronRDP", features = ["server", "svc", "cliprdr"] }

View File

@ -1,5 +1,5 @@
use clap::clap_derive::ValueEnum;
use clap::{crate_name, Parser};
use clap::Parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum SecurityProtocol {
@ -43,9 +43,6 @@ pub struct ServerArgs {
#[derive(Parser, Debug)]
pub struct Args {
#[clap(short, long, value_parser, default_value_t = format!("{}.log", crate_name!()))]
pub log_file: String,
#[clap(flatten)]
pub server: ServerArgs,

View File

@ -3,12 +3,13 @@ use qemu_display::zbus;
mod args;
mod server;
mod util;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let mut args = args::parse();
setup_logging(args.log_file.as_str()).context("unable to initialize logging")?;
setup_logging().context("unable to initialize logging")?;
let dbus = match args.dbus_address.take() {
None => zbus::Connection::session().await,
@ -25,27 +26,16 @@ async fn main() -> Result<(), anyhow::Error> {
Ok(())
}
fn setup_logging(log_file: &str) -> anyhow::Result<()> {
use std::fs::OpenOptions;
fn setup_logging() -> anyhow::Result<()> {
use tracing::metadata::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)
.with_context(|| format!("couldnt open {log_file}"))?;
let fmt_layer = tracing_subscriber::fmt::layer()
.compact()
.with_ansi(false)
.with_writer(file);
let fmt_layer = tracing_subscriber::fmt::layer().compact();
let env_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.with_env_var("QEMURDP_LOG_LEVEL")
.with_env_var("QEMURDP_LOG")
.from_env_lossy();
tracing_subscriber::registry()

View File

@ -0,0 +1,445 @@
use std::sync::{Arc, Mutex};
use anyhow::Result;
use ironrdp::{
cliprdr::{
backend::{ClipboardMessage, CliprdrBackend, CliprdrBackendFactory},
pdu::{
ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags,
FileContentsRequest, FileContentsResponse, FormatDataRequest, FormatDataResponse,
LockDataId, OwnedFormatDataResponse,
},
},
pdu::{
cursor::ReadCursor,
utils::{read_string_from_cursor, CharacterSet},
},
server::{CliprdrServerFactory, ServerEvent, ServerEventSender},
svc::impl_as_any,
};
use tracing::{debug, error, warn};
use qemu_display::{zbus, Clipboard, ClipboardSelection};
use tokio::{
sync::{
mpsc::{self, Receiver, Sender},
oneshot,
},
task,
};
#[derive(Debug)]
pub struct Inner {
clipboard: Clipboard,
tx: Sender<ClipboardEvent>,
selection: ClipboardSelection,
serial: u32,
ev_sender: Option<mpsc::UnboundedSender<ServerEvent>>,
dbus_request: Option<(oneshot::Sender<Vec<u8>>, ClipboardFormatId)>,
_task: Option<task::JoinHandle<()>>,
}
#[derive(Clone, Debug)]
pub struct ClipboardHandler {
inner: Arc<Mutex<Inner>>,
}
#[async_trait::async_trait]
impl qemu_display::ClipboardHandler for ClipboardHandler {
async fn register(&mut self) {
let mut inner = self.inner.lock().unwrap();
inner.serial = 0;
inner.dbus_request = None;
}
async fn unregister(&mut self) {}
async fn grab(&mut self, selection: ClipboardSelection, serial: u32, mimes: Vec<String>) {
debug!(?selection, ?serial, "Grab -> RDP");
let mut inner = self.inner.lock().unwrap();
if selection != inner.selection {
return;
}
if serial < inner.serial {
warn!(?serial, ?inner.serial, "discarding");
return;
}
inner.serial = serial;
let format_list = mimes
.iter()
.filter_map(|f| match f.as_str() {
"text/plain;charset=utf-8" => {
let cf = ClipboardFormat::new(ClipboardFormatId::CF_UNICODETEXT);
Some(cf)
}
_ => None,
})
.collect();
if let Some(svc_sender) = inner.ev_sender.as_mut() {
if let Err(e) = svc_sender.send(ServerEvent::Clipboard(
ClipboardMessage::SendInitiateCopy(format_list),
)) {
error!(?e, "failed to send SVC message");
}
}
}
async fn release(&mut self, selection: ClipboardSelection) {
debug!(?selection, "Release -> RDP");
let mut inner = self.inner.lock().unwrap();
if selection != inner.selection {
return;
}
if let Some(svc_sender) = inner.ev_sender.as_mut() {
if let Err(e) = svc_sender.send(ServerEvent::Clipboard(
ClipboardMessage::SendInitiateCopy(vec![]),
)) {
error!(?e, "failed to send SVC message");
}
}
}
async fn request(
&mut self,
selection: ClipboardSelection,
mimes: Vec<String>,
) -> qemu_display::Result<(String, Vec<u8>)> {
debug!(?selection, ?mimes, "Request -> RDP");
let (rx, format) = {
let mut inner = self.inner.lock().unwrap();
if selection != inner.selection {
return Err(qemu_display::Error::Failed(
"Unhandled clipboard selection".into(),
));
}
// since we are blocking on Request method handling, we shouldn't get there again
if inner.dbus_request.is_some() {
return Err(qemu_display::Error::Failed(
"Pending clipboard request!".into(),
));
}
let format = {
let mut format_found = None;
for mime in mimes.iter() {
match mime.as_str() {
"text/plain;charset=utf-8" => {
format_found = Some(ClipboardFormatId::CF_UNICODETEXT);
break;
}
_ => continue,
}
}
format_found
};
let Some(format) = format else {
return Err(qemu_display::Error::Failed("Unhandled MIMEs".into()));
};
let (tx, rx) = oneshot::channel();
inner.dbus_request = Some((tx, format));
if let Some(svc_sender) = inner.ev_sender.as_mut() {
if let Err(e) = svc_sender.send(ServerEvent::Clipboard(
ClipboardMessage::SendInitiatePaste(format),
)) {
error!(?e, "failed to send SVC message");
}
}
(rx, format)
};
let data = rx
.await
.map_err(|_| qemu_display::Error::Failed("Failed to get clipboard data".into()))?;
let data = match format {
ClipboardFormatId::CF_UNICODETEXT => {
let mut cursor = ReadCursor::new(&data);
let s = read_string_from_cursor(&mut cursor, CharacterSet::Unicode, true)
.map_err(|_| qemu_display::Error::Failed("Failed to convert string".into()))?;
s.into_bytes()
}
_ => unimplemented!(),
};
Ok(("text/plain;charset=utf-8".into(), data))
}
}
#[derive(Debug)]
enum ClipboardEvent {
Register,
Grab {
selection: ClipboardSelection,
serial: u32,
available_formats: Vec<ClipboardFormat>,
},
Release {
selection: ClipboardSelection,
},
Request {
selection: ClipboardSelection,
request: FormatDataRequest,
},
}
async fn rdp_clipboard_receive_task(mut rx: Receiver<ClipboardEvent>, cb: ClipboardHandler) {
let clipboard = {
let inner = cb.inner.lock().unwrap();
inner.clipboard.clone()
};
loop {
let res = match rx.recv().await {
Some(ClipboardEvent::Register) => {
debug!("Register -> dbus");
clipboard.proxy.register().await
}
Some(ClipboardEvent::Grab {
selection,
serial,
available_formats,
}) => {
let mimes = available_formats
.iter()
.filter_map(|f| match f.id {
ClipboardFormatId::CF_UNICODETEXT => {
Some("text/plain;charset=utf-8".to_string())
}
_ => None,
})
.collect::<Vec<_>>();
debug!(?mimes, "Grab -> dbus");
let mimes: Vec<&str> = mimes.iter().map(AsRef::as_ref).collect();
clipboard.proxy.grab(selection, serial, &mimes).await
}
Some(ClipboardEvent::Release { selection }) => {
debug!("Release -> dbus");
clipboard.proxy.release(selection).await
}
Some(ClipboardEvent::Request { selection, request }) => {
debug!(?request, "Request -> dbus");
let mime = match request.format {
ClipboardFormatId::CF_UNICODETEXT => "text/plain;charset=utf-8",
fmt => {
debug!(?fmt, "unhandled requested format");
continue;
}
};
let res = clipboard.proxy.request(selection, &[mime]).await;
if let Ok(res) = res {
let data = match (request.format, res.0.as_str()) {
(ClipboardFormatId::CF_UNICODETEXT, "text/plain;charset=utf-8") => {
let Ok(s) = std::str::from_utf8(&res.1) else {
error!("Invalid text data");
continue;
};
OwnedFormatDataResponse::new_unicode_string(s)
}
(_, mime) => {
debug!(?mime, "Unsupported data format");
continue;
}
};
let mut inner = cb.inner.lock().unwrap();
if let Some(svc_sender) = inner.ev_sender.as_mut() {
if let Err(e) = svc_sender.send(ServerEvent::Clipboard(
ClipboardMessage::SendFormatData(data),
)) {
error!(?e, "failed to send SVC message");
}
}
} else {
warn!(?res, "Request dbus reply");
}
Ok(())
}
None => break,
};
if let Err(e) = res {
error!(?e, "input handling error");
}
}
}
impl Inner {
fn register(&mut self) {
if let Err(e) = self.tx.try_send(ClipboardEvent::Register) {
error!(?e, "clipboard register error");
}
}
fn grab(&mut self, available_formats: Vec<ClipboardFormat>) {
if let Err(e) = self.tx.try_send(ClipboardEvent::Grab {
selection: self.selection,
serial: self.serial,
available_formats,
}) {
error!(?e, "clipboard grab error");
} else {
self.serial += 1;
}
}
fn release(&mut self) {
if let Err(e) = self.tx.try_send(ClipboardEvent::Release {
selection: self.selection,
}) {
error!(?e, "clipboard release error");
}
}
fn request(&mut self, request: FormatDataRequest) {
if let Err(e) = self.tx.try_send(ClipboardEvent::Request {
selection: self.selection,
request,
}) {
error!(?e, "clipboard request error");
}
}
fn data(&mut self, data: Vec<u8>) {
debug!("Data -> dbus");
if let Some((data_tx, _fmt)) = self.dbus_request.take() {
if let Err(e) = data_tx.send(data) {
error!(?e, "failed to send clipboard data to dbus");
}
}
}
}
impl ClipboardHandler {
pub async fn connect(dbus: zbus::Connection) -> Result<Self> {
let clipboard = Clipboard::new(&dbus).await?;
let selection = ClipboardSelection::Clipboard;
let (tx, rx) = tokio::sync::mpsc::channel(30);
let inner = Arc::new(Mutex::new(Inner {
tx,
selection,
serial: 0,
clipboard,
ev_sender: None,
dbus_request: None,
_task: None,
}));
let s = Self { inner };
let clone = s.clone();
let _task = task::spawn(async move { rdp_clipboard_receive_task(rx, clone).await });
s.inner.lock().unwrap()._task = Some(_task);
Ok(s)
}
}
// impl Server
impl ServerEventSender for ClipboardHandler {
fn set_sender(&mut self, sender: mpsc::UnboundedSender<ServerEvent>) {
let mut inner = self.inner.lock().unwrap();
inner.ev_sender = Some(sender);
}
}
impl CliprdrServerFactory for ClipboardHandler {}
#[derive(Debug)]
pub(crate) struct RDPCliprdrBackend {
inner: Arc<Mutex<Inner>>,
}
impl_as_any!(RDPCliprdrBackend);
impl CliprdrBackendFactory for ClipboardHandler {
fn build_cliprdr_backend(&self) -> Box<dyn CliprdrBackend> {
Box::new(RDPCliprdrBackend {
inner: self.inner.clone(),
})
}
}
impl CliprdrBackend for RDPCliprdrBackend {
fn temporary_directory(&self) -> &str {
".cliprdr"
}
fn client_capabilities(&self) -> ClipboardGeneralCapabilityFlags {
self.inner.lock().unwrap().register();
// No additional capabilities yet
ClipboardGeneralCapabilityFlags::empty()
}
fn on_process_negotiated_capabilities(
&mut self,
capabilities: ClipboardGeneralCapabilityFlags,
) {
debug!(?capabilities);
}
fn on_remote_copy(&mut self, available_formats: &[ClipboardFormat]) {
debug!(?available_formats);
let mut inner = self.inner.lock().unwrap();
if available_formats.is_empty() {
inner.release();
} else {
inner.grab(available_formats.into());
}
}
fn on_format_data_request(&mut self, request: FormatDataRequest) {
debug!(?request);
self.inner.lock().unwrap().request(request);
}
fn on_format_data_response(&mut self, response: FormatDataResponse<'_>) {
debug!(?response);
self.inner.lock().unwrap().data(response.into_data().into());
}
fn on_file_contents_request(&mut self, request: FileContentsRequest) {
debug!(?request);
}
fn on_file_contents_response(&mut self, response: FileContentsResponse<'_>) {
debug!(?response);
}
fn on_lock(&mut self, data_id: LockDataId) {
debug!(?data_id);
}
fn on_unlock(&mut self, data_id: LockDataId) {
debug!(?data_id);
}
fn on_request_format_list(&mut self) {
debug!("on_request_format_list");
}
}

View File

@ -1,23 +1,47 @@
use anyhow::Result;
use ironrdp::connector::DesktopSize;
use qemu_display::{zbus, Console, ConsoleListenerHandler, Cursor, MouseSet, Scanout, Update};
use ironrdp::server::{BitmapUpdate, DisplayUpdate, PixelFormat, PixelOrder, RdpServerDisplay};
use ironrdp::connector::DesktopSize;
use ironrdp::server::{
BitmapUpdate, DisplayUpdate, PixelOrder, RGBAPointer, RdpServerDisplay, RdpServerDisplayUpdates,
};
use crate::{cast, util::PixmanFormat};
pub struct DisplayHandler {
console: Console,
}
struct DisplayUpdates {
receiver: tokio::sync::mpsc::Receiver<DisplayUpdate>,
}
impl DisplayHandler {
pub async fn connect(dbus: zbus::Connection) -> Result<Self> {
let (sender, receiver) = tokio::sync::mpsc::channel::<DisplayUpdate>(32);
let listener = Listener::new(sender);
let console = Console::new(&dbus, 0).await?;
console.register_listener(listener).await?;
Ok(Self { console, receiver })
Ok(Self { console })
}
async fn listen(&self) -> Result<DisplayUpdates> {
let (sender, receiver) = tokio::sync::mpsc::channel::<DisplayUpdate>(32);
let (width, height) = (
self.console.width().await? as _,
self.console.height().await? as _,
);
let desktop_size = DesktopSize { width, height };
let listener = Listener::new(sender, desktop_size);
self.console.unregister_listener();
self.console.register_listener(listener).await?;
Ok(DisplayUpdates { receiver })
}
}
#[async_trait::async_trait]
impl RdpServerDisplayUpdates for DisplayUpdates {
async fn next_update(&mut self) -> Option<DisplayUpdate> {
self.receiver.recv().await
}
}
@ -29,18 +53,24 @@ impl RdpServerDisplay for DisplayHandler {
DesktopSize { height, width }
}
async fn get_update(&mut self) -> Option<DisplayUpdate> {
self.receiver.recv().await
async fn updates(&mut self) -> Result<Box<dyn RdpServerDisplayUpdates>> {
Ok(Box::new(self.listen().await?))
}
}
struct Listener {
sender: tokio::sync::mpsc::Sender<DisplayUpdate>,
_desktop_size: DesktopSize,
cursor_hot: (i32, i32),
}
impl Listener {
fn new(sender: tokio::sync::mpsc::Sender<DisplayUpdate>) -> Self {
Self { sender }
fn new(sender: tokio::sync::mpsc::Sender<DisplayUpdate>, _desktop_size: DesktopSize) -> Self {
Self {
sender,
_desktop_size,
cursor_hot: (0, 0),
}
}
async fn send(&mut self, update: DisplayUpdate) {
@ -53,36 +83,45 @@ impl Listener {
#[async_trait::async_trait]
impl ConsoleListenerHandler for Listener {
async fn scanout(&mut self, scanout: Scanout) {
let format = match scanout.format {
537_004_168 => PixelFormat::BgrA32,
_ => PixelFormat::RgbA32,
let desktop_size = DesktopSize {
width: cast!(scanout.width),
height: cast!(scanout.height),
};
let bitmap = DisplayUpdate::Bitmap(BitmapUpdate {
top: 0,
left: 0,
width: scanout.width,
height: scanout.height,
format,
order: PixelOrder::TopToBottom,
data: scanout.data,
});
tracing::debug!(?desktop_size);
self.send(bitmap).await;
// if desktop_size != self.desktop_size {
// self.desktop_size = desktop_size;
// self.send(DisplayUpdate::Resize(desktop_size)).await;
// }
self.update(Update {
x: 0,
y: 0,
w: cast!(scanout.width),
h: cast!(scanout.height),
stride: scanout.stride,
format: scanout.format,
data: scanout.data,
})
.await
}
async fn update(&mut self, update: Update) {
let format = match update.format {
537_004_168 => PixelFormat::BgrA32,
_ => PixelFormat::RgbA32,
let Ok(format) = PixmanFormat(update.format).try_into() else {
println!("Unhandled format {}", update.format);
return;
};
let width: u16 = cast!(update.w);
let height: u16 = cast!(update.h);
let bitmap = DisplayUpdate::Bitmap(BitmapUpdate {
// TODO: fix scary conversion
top: update.y as u32,
left: update.x as u32,
width: update.w as u32,
height: update.h as u32,
left: cast!(update.x),
top: cast!(update.y),
width: cast!(width),
height: cast!(height),
format,
order: PixelOrder::TopToBottom,
data: update.data,
@ -92,15 +131,72 @@ impl ConsoleListenerHandler for Listener {
}
#[cfg(unix)]
async fn scanout_dmabuf(&mut self, _scanout: qemu_display::ScanoutDMABUF) {}
async fn scanout_dmabuf(&mut self, scanout: qemu_display::ScanoutDMABUF) {
tracing::debug!(?scanout);
}
#[cfg(unix)]
async fn update_dmabuf(&mut self, _update: qemu_display::UpdateDMABUF) {}
async fn update_dmabuf(&mut self, update: qemu_display::UpdateDMABUF) {
tracing::debug!(?update);
}
async fn disable(&mut self) {
tracing::debug!("disable");
}
async fn mouse_set(&mut self, set: MouseSet) {
tracing::debug!(?set);
// FIXME: this create weird effects on the client
//
// self.send(DisplayUpdate::PointerPosition(ironrdp_pdu::PointerPositionAttribute {
// x: cast!(set.x + self.cursor_hot.0),
// y: cast!(set.y + self.cursor_hot.1),
// }))
// .await;
//
if set.on == 0 {
self.send(DisplayUpdate::HidePointer).await;
}
}
async fn cursor_define(&mut self, cursor: Cursor) {
tracing::debug!(?cursor);
self.cursor_hot = (cursor.hot_x, cursor.hot_y);
fn flip_vertically(image: Vec<u8>, width: usize, height: usize) -> Vec<u8> {
let row_length = width * 4; // 4 bytes per pixel
let mut flipped_image = vec![0; image.len()]; // Initialize a vector for the flipped image
for y in 0..height {
let source_row_start = y * row_length;
let dest_row_start = (height - 1 - y) * row_length;
// Copy the whole row from the source position to the destination position
flipped_image[dest_row_start..dest_row_start + row_length]
.copy_from_slice(&image[source_row_start..source_row_start + row_length]);
}
flipped_image
}
let data = flip_vertically(cursor.data, cast!(cursor.width), cast!(cursor.height));
self.send(DisplayUpdate::RGBAPointer(RGBAPointer {
width: cast!(cursor.width),
height: cast!(cursor.height),
hot_x: cast!(cursor.hot_x),
hot_y: cast!(cursor.hot_y),
data,
}))
.await;
}
fn disconnected(&mut self) {
tracing::debug!("console listener disconnected");
}
async fn disable(&mut self) {}
async fn mouse_set(&mut self, _set: MouseSet) {}
async fn cursor_define(&mut self, _cursor: Cursor) {}
fn disconnected(&mut self) {}
fn interfaces(&self) -> Vec<String> {
vec![]
}

View File

@ -1,74 +1,89 @@
use qemu_display::{zbus, Console, KeyboardProxy, MouseButton, MouseProxy};
use qemu_display::{zbus, Console, MouseButton};
use ironrdp::server::{KeyboardEvent, MouseEvent, RdpServerInputHandler};
use tokio::{
sync::mpsc::{Receiver, Sender},
task,
};
pub struct InputHandler<'a> {
pos: (u16, u16),
mouse: MouseProxy<'a>,
keyboard: KeyboardProxy<'a>,
use crate::cast;
pub struct InputHandler {
tx: Sender<InputEvent>,
_task: task::JoinHandle<()>,
}
#[async_trait::async_trait]
impl<'a> RdpServerInputHandler for InputHandler<'a> {
async fn keyboard(&mut self, event: KeyboardEvent) {
let result = match event {
KeyboardEvent::Pressed { code, .. } => self.keyboard.press(code as u32).await,
KeyboardEvent::Released { code, .. } => self.keyboard.release(code as u32).await,
other => {
eprintln!("unhandled keyboard event: {:?}", other);
Ok(())
}
};
#[derive(Debug)]
enum InputEvent {
Keyboard(KeyboardEvent),
Mouse(MouseEvent),
}
if let Err(e) = result {
impl RdpServerInputHandler for InputHandler {
fn keyboard(&mut self, event: KeyboardEvent) {
tracing::debug!(?event);
if let Err(e) = self.tx.try_send(InputEvent::Keyboard(event)) {
eprintln!("keyboard error: {:?}", e);
}
}
async fn mouse(&mut self, event: MouseEvent) {
let result = match event {
MouseEvent::Move { x, y } => self.mouse_move(x, y).await,
MouseEvent::RightPressed => self.mouse.press(MouseButton::Right).await,
MouseEvent::RightReleased => self.mouse.release(MouseButton::Right).await,
MouseEvent::LeftPressed => self.mouse.press(MouseButton::Left).await,
MouseEvent::LeftReleased => self.mouse.release(MouseButton::Left).await,
MouseEvent::VerticalScroll { value } => {
let motion = if value > 0 {
MouseButton::WheelUp
} else {
MouseButton::WheelDown
};
self.mouse.press(motion).await
}
};
if let Err(e) = result {
eprintln!("keyboard error: {:?}", e);
fn mouse(&mut self, event: MouseEvent) {
tracing::debug!(?event);
if let Err(e) = self.tx.try_send(InputEvent::Mouse(event)) {
eprintln!("mouse error: {:?}", e);
}
}
}
impl<'a> InputHandler<'a> {
pub async fn connect(dbus: zbus::Connection) -> anyhow::Result<InputHandler<'a>> {
async fn input_receive_task(mut rx: Receiver<InputEvent>, console: Console) {
loop {
let res = match rx.recv().await {
Some(InputEvent::Keyboard(ev)) => match ev {
KeyboardEvent::Pressed { code, .. } => console.keyboard.press(code as u32).await,
KeyboardEvent::Released { code, .. } => console.keyboard.release(code as u32).await,
other => {
eprintln!("unhandled keyboard event: {:?}", other);
Ok(())
}
},
Some(InputEvent::Mouse(ev)) => match ev {
MouseEvent::Move { x, y } => {
tracing::debug!(?x, ?y);
console.mouse.set_abs_position(cast!(x), cast!(y)).await
}
MouseEvent::RightPressed => console.mouse.press(MouseButton::Right).await,
MouseEvent::RightReleased => console.mouse.release(MouseButton::Right).await,
MouseEvent::LeftPressed => console.mouse.press(MouseButton::Left).await,
MouseEvent::LeftReleased => console.mouse.release(MouseButton::Left).await,
MouseEvent::VerticalScroll { value } => {
let motion = if value > 0 {
MouseButton::WheelUp
} else {
MouseButton::WheelDown
};
console.mouse.press(motion).await
}
other => {
eprintln!("unhandled input event: {:?}", other);
Ok(())
}
},
None => break,
};
if let Err(e) = res {
eprintln!("input handling error: {:?}", e);
}
}
}
impl InputHandler {
pub async fn connect(dbus: zbus::Connection) -> anyhow::Result<InputHandler> {
let console = Console::new(&dbus, 0).await?;
let (tx, rx) = tokio::sync::mpsc::channel(30);
let _task = task::spawn(async move { input_receive_task(rx, console).await });
Ok(Self {
pos: (0, 0),
mouse: console.mouse,
keyboard: console.keyboard,
})
}
pub async fn mouse_move(&mut self, x: u16, y: u16) -> Result<(), zbus::Error> {
if self.mouse.is_absolute().await.unwrap_or(true) {
self.mouse.set_abs_position(x.into(), y.into()).await
} else {
let (dx, dy) = (x as i32 - self.pos.0 as i32, y as i32 - self.pos.1 as i32);
let res = self.mouse.rel_motion(dx, dy).await;
self.pos = (x, y);
res
}
Ok(Self { _task, tx })
}
}

View File

@ -1,3 +1,4 @@
mod clipboard;
mod display;
mod input;
@ -12,6 +13,7 @@ use ironrdp::server::RdpServer;
use crate::args::ServerArgs;
use clipboard::ClipboardHandler;
use display::DisplayHandler;
use input::InputHandler;
@ -35,12 +37,14 @@ impl Server {
let handler = InputHandler::connect(self.dbus.clone()).await?;
let display = DisplayHandler::connect(self.dbus.clone()).await?;
let clipboard = ClipboardHandler::connect(self.dbus.clone()).await?;
let mut server = RdpServer::builder()
.with_addr((self.args.address, self.args.port))
.with_tls(tls.unwrap())
.with_input_handler(handler)
.with_display_handler(display)
.with_cliprdr_factory(Some(Box::new(clipboard)))
.build();
server.run().await
@ -51,11 +55,14 @@ fn acceptor(cert_path: &str, key_path: &str) -> Result<TlsAcceptor, Error> {
let cert = certs(&mut BufReader::new(File::open(cert_path)?))?[0].clone();
let key = pkcs8_private_keys(&mut BufReader::new(File::open(key_path)?))?[0].clone();
let server_config = ServerConfig::builder()
let mut server_config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(vec![rustls::Certificate(cert)], rustls::PrivateKey(key))
.expect("bad certificate/key");
// This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret)
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
Ok(TlsAcceptor::from(Arc::new(server_config)))
}

46
qemu-rdp/src/util.rs Normal file
View File

@ -0,0 +1,46 @@
use ironrdp::server::PixelFormat;
#[macro_export]
macro_rules! cast {
($value:expr) => {
match $value.try_into() {
Ok(val) => val,
Err(err) => {
eprintln!("Error casting value: {}", err);
return;
}
}
};
}
pub(crate) struct PixmanFormat(pub u32);
#[cfg(target_endian = "little")]
impl TryFrom<PixmanFormat> for PixelFormat {
type Error = ();
fn try_from(value: PixmanFormat) -> Result<Self, Self::Error> {
use pixman_sys::*;
#[allow(non_upper_case_globals)]
match value.0 {
pixman_format_code_t_PIXMAN_x8r8g8b8 => Ok(PixelFormat::BgrX32),
_ => Err(()),
}
}
}
#[cfg(target_endian = "big")]
impl TryFrom<PixmanFormat> for PixelFormat {
type Error = ();
fn try_from(value: PixmanFormat) -> Result<Self, Self::Error> {
use pixman_sys::*;
#[allow(non_upper_case_globals)]
match value.0 {
pixman_format_code_t_PIXMAN_x8r8g8b8 => Ok(PixelFormat::XRgb32),
_ => Err(()),
}
}
}

View File

@ -221,9 +221,7 @@ fn watch_clipboard(
}
fn clipboard_from_selection(selection: ClipboardSelection) -> Option<(gdk::Clipboard, usize)> {
let Some(display) = gdk::Display::default() else {
return None;
};
let display = gdk::Display::default()?;
match selection {
ClipboardSelection::Clipboard => Some((display.clipboard(), 0)),