Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 65 additions & 19 deletions crates/ironrdp-rdpeai/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ use tracing::{debug, trace, warn};
use crate::CHANNEL_NAME;
use crate::pdu::{DataPdu, FormatChangePdu, FormatsPdu, OpenPdu, OpenReplyPdu, RdpeaiPdu, Version, VersionPdu};

pub trait RdpeaiError: core::error::Error + Send + Sync + 'static {}

impl<T> RdpeaiError for T where T: core::error::Error + Send + Sync + 'static {}

/// Message sent by the embedding application's event loop to drive [`RdpeaiServer`] from
/// outside the DVC message-processing path — e.g. once a consumer decides it wants to start
/// recording, or wants to switch formats mid-session.
#[derive(Debug)]
pub enum RdpeaiServerMessage {
/// Request the client start recording. See [`RdpeaiServer::open`].
Open {
frames_per_packet: u32,
initial_format: u32,
capture_format: AudioFormat,
},
/// Request the client switch to a different negotiated format. See
/// [`RdpeaiServer::change_format`].
ChangeFormat { new_format: u32 },
/// Failure received from the embedding application's own capture/consumer pipeline.
/// Implementations should log/display this error.
Error(Box<dyn RdpeaiError>),
}

/// Handler for the server side of the Audio Input Redirection Virtual Channel (`AUDIO_INPUT`).
///
/// Implementations supply the list of audio formats the server offers and receive the
Expand All @@ -31,7 +54,9 @@ pub trait RdpeaiServerBackend: Send {
}

/// Called when the client answers an Open PDU (MS-RDPEAI 3.3.5.1.8). `result` is the
/// client's raw HRESULT; `result == 0` ([`OpenReplyPdu::S_OK`]) means capture started.
/// client's raw HRESULT; per MS-RDPEAI 3.3.5.1.8 an HRESULT is an error only when its
/// sign bit is set, so `result >= 0` (not just [`OpenReplyPdu::S_OK`]) means capture
/// started.
fn on_open_reply(&mut self, result: i32) {
let _ = result;
}
Expand Down Expand Up @@ -323,24 +348,45 @@ impl RdpeaiServer {
}

fn handle_open_reply(&mut self, pdu: OpenReplyPdu) -> PduResult<Vec<DvcMessage>> {
if self.state != State::AwaitingOpenReply {
warn!(?self.state, "Ignoring out-of-sequence AUDIO_INPUT OpenReply PDU");
return Ok(Vec::new());
}

self.backend.on_open_reply(pdu.result);
if pdu.result == OpenReplyPdu::S_OK {
self.state = State::Opened;
debug!("AUDIO_INPUT capture opened");
} else {
// MS-RDPEAI 3.3.5.1.8: on failure the server MAY send another Open PDU; leave that
// to the caller by returning to Ready rather than retrying automatically.
self.current_format = None;
self.state = State::Ready;
warn!(
result = pdu.result,
"AUDIO_INPUT client failed to open its capture device"
);
match self.state {
State::AwaitingOpenReply => {
self.backend.on_open_reply(pdu.result);
// MS-RDPEAI 3.3.5.1.8: an HRESULT is an error only when its sign bit is set,
// so any non-negative result (not just S_OK) is success.
if pdu.result >= 0 {
self.state = State::Opened;
debug!("AUDIO_INPUT capture opened");
} else {
// MS-RDPEAI 3.3.5.1.8: on failure the server MAY send another Open PDU;
// leave that to the caller by returning to Ready rather than retrying
// automatically.
self.current_format = None;
self.state = State::Ready;
warn!(
result = pdu.result,
"AUDIO_INPUT client failed to open its capture device"
);
}
}
State::AwaitingFormatConfirm => {
// A client rejecting Open before confirming the initial format (e.g.
// initialFormat out of range, or FramesPerPacket rejected) skips the
// FormatChange confirm and replies with OpenReply failure directly.
// 3.3.5.1.8 only conditions the server's reaction on the Result field, not on
// a preceding FormatChange, so accept it here too rather than leaving the
// channel wedged in AwaitingFormatConfirm with no path back to Ready.
self.backend.on_open_reply(pdu.result);
self.pending_open_format = None;
self.current_format = None;
self.state = State::Ready;
warn!(
result = pdu.result,
"AUDIO_INPUT client rejected Open before confirming the initial format"
);
}
_ => {
warn!(?self.state, "Ignoring out-of-sequence AUDIO_INPUT OpenReply PDU");
}
}
Ok(Vec::new())
}
Comment thread
glamberson marked this conversation as resolved.
Expand Down
1 change: 1 addition & 0 deletions crates/ironrdp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public
ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.7" } # public
ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.9" } # public
ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" } # public
ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1" } # public
ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = true }
ironrdp-usb = { path = "../ironrdp-usb", version = "0.1", optional = true }
tracing = { version = "0.1", features = ["log"] }
Expand Down
15 changes: 14 additions & 1 deletion crates/ironrdp-server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use super::server::{
use crate::error::ServerResult;
#[cfg(feature = "usb")]
use crate::urbdrc::DeviceFactory;
use crate::{DisplayUpdate, RdpServerDisplayUpdates, RdpdrServerFactory, RdpeiServerFactory, SoundServerFactory};
use crate::{
DisplayUpdate, RdpServerDisplayUpdates, RdpdrServerFactory, RdpeaiServerFactory, RdpeiServerFactory,
SoundServerFactory,
};

pub struct WantsAddr {}
pub struct WantsSecurity {
Expand All @@ -46,6 +49,7 @@ pub struct BuilderDone {
sound_factory: Option<Box<dyn SoundServerFactory>>,
rdpei_factory: Option<Box<dyn RdpeiServerFactory>>,
rdpdr_factory: Option<Box<dyn RdpdrServerFactory>>,
rdpeai_factory: Option<Box<dyn RdpeaiServerFactory>>,
connection_handler: Option<Box<dyn ConnectionHandler>>,
credential_validator: Option<Arc<dyn CredentialValidator>>,
#[cfg(feature = "egfx")]
Expand Down Expand Up @@ -156,6 +160,7 @@ impl RdpServerBuilder<WantsDisplay> {
cliprdr_factory: None,
rdpei_factory: None,
rdpdr_factory: None,
rdpeai_factory: None,
connection_handler: None,
credential_validator: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
Expand Down Expand Up @@ -189,6 +194,7 @@ impl RdpServerBuilder<WantsDisplay> {
cliprdr_factory: None,
rdpei_factory: None,
rdpdr_factory: None,
rdpeai_factory: None,
connection_handler: None,
credential_validator: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
Expand Down Expand Up @@ -239,6 +245,12 @@ impl RdpServerBuilder<BuilderDone> {
self
}

/// Configure MS-RDPEAI (audio input / microphone redirection over a dynamic channel).
pub fn with_rdpeai_factory(mut self, rdpeai_factory: Option<Box<dyn RdpeaiServerFactory>>) -> Self {
self.state.rdpeai_factory = rdpeai_factory;
self
}

/// Configure EGFX (Graphics Pipeline Extension) for H.264 video streaming.
#[cfg(feature = "egfx")]
pub fn with_gfx_factory(mut self, gfx_factory: Option<Box<dyn GfxServerFactory>>) -> Self {
Expand Down Expand Up @@ -471,6 +483,7 @@ impl RdpServerBuilder<BuilderDone> {
self.state.cliprdr_factory,
self.state.rdpei_factory,
self.state.rdpdr_factory,
self.state.rdpeai_factory,
self.state.connection_handler,
#[cfg(feature = "egfx")]
self.state.gfx_factory,
Expand Down
2 changes: 2 additions & 0 deletions crates/ironrdp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod heartbeat;
#[cfg(feature = "helper")]
mod helper;
mod rdpdr;
mod rdpeai;
mod rdpei;
mod server;
mod sound;
Expand All @@ -43,6 +44,7 @@ pub use ironrdp_pdu::rdp::session_info::ServerAutoReconnect;
#[cfg(feature = "usb")]
pub use ironrdp_rdpeusb::io::{CompletionData, DeviceAnnounce, DeviceText, InternalIoControlPacket};
pub use rdpdr::{NoopRdpdrServerBackend, RdpdrServerBackend, RdpdrServerFactory, RdpdrServerMessage};
pub use rdpeai::{NoopRdpeaiServerBackend, RdpeaiServerBackend, RdpeaiServerFactory, RdpeaiServerMessage};
pub use rdpei::{
CsReadyFlags, CsReadyPdu, DismissHoveringTouchContactPdu, PenContact, PenContactDataFlags, PenContactFields,
PenContactFlags, PenEventPdu, PenFlags, PenFrame, RdpInputProtocolVersion, RdpeiHandler, RdpeiServer,
Expand Down
7 changes: 7 additions & 0 deletions crates/ironrdp-server/src/rdpeai.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub use ironrdp_rdpeai::server::{NoopRdpeaiServerBackend, RdpeaiServerBackend, RdpeaiServerMessage};

use crate::ServerEventSender;

pub trait RdpeaiServerFactory: ServerEventSender {
fn build_backend(&self) -> Box<dyn RdpeaiServerBackend>;
}
79 changes: 79 additions & 0 deletions crates/ironrdp-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, Se
use ironrdp_pdu::x224::X224;
use ironrdp_pdu::{Action, PduResult, decode_err, mcs, nego, rdp};
use ironrdp_rdpdr as rdpdr;
use ironrdp_rdpeai as rdpeai;
use ironrdp_rdpsnd as rdpsnd;
use ironrdp_svc::{ChannelFlags, StaticChannelId, StaticChannelSet, SvcProcessor, server_encode_svc_messages};
use ironrdp_tokio::{FramedRead, FramedWrite, TokioFramed, split_tokio_framed, unsplit_tokio_framed};
use rand::RngCore as _;
use rdpdr::server::{RdpdrServer, RdpdrServerMessage};
use rdpeai::server::{RdpeaiServer, RdpeaiServerMessage};
use rdpsnd::server::{RdpsndServer, RdpsndServerMessage};
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt as _};
use tokio::net::{TcpSocket, TcpStream};
Expand All @@ -58,6 +60,7 @@ use crate::error::{ServerError, ServerErrorExt as _, ServerErrorKind, ServerResu
use crate::gfx::{EgfxServerMessage, GfxServerFactory};
use crate::handler::RdpServerInputHandler;
use crate::heartbeat::HeartbeatConfig;
use crate::rdpeai::RdpeaiServerFactory;
use crate::rdpei::RdpeiServerFactory;
#[cfg(feature = "usb")]
use crate::urbdrc::{
Expand Down Expand Up @@ -670,6 +673,7 @@ pub struct RdpServer {
cliprdr_factory: Option<Box<dyn CliprdrServerFactory>>,
rdpei_factory: Option<Box<dyn RdpeiServerFactory>>,
rdpdr_factory: Option<Box<dyn RdpdrServerFactory>>,
rdpeai_factory: Option<Box<dyn RdpeaiServerFactory>>,
echo_handle: EchoServerHandle,
#[cfg(feature = "egfx")]
gfx_factory: Option<Box<dyn GfxServerFactory>>,
Expand Down Expand Up @@ -838,6 +842,7 @@ pub enum ServerEvent {
Clipboard(ClipboardMessage),
Rdpsnd(RdpsndServerMessage),
Rdpdr(RdpdrServerMessage),
Rdpeai(RdpeaiServerMessage),
Echo(EchoServerMessage),
SetCredentials(Credentials),
/// Replace or clear the Server Auto-Reconnect Cookie.
Expand Down Expand Up @@ -869,6 +874,7 @@ impl fmt::Debug for ServerEvent {
Self::Clipboard(..) => f.write_str("Clipboard(..)"),
Self::Rdpsnd(..) => f.write_str("Rdpsnd(..)"),
Self::Rdpdr(..) => f.write_str("Rdpdr(..)"),
Self::Rdpeai(..) => f.write_str("Rdpeai(..)"),
Self::Echo(..) => f.write_str("Echo(..)"),
Self::SetCredentials(..) => f.write_str("SetCredentials(..)"),
Self::SetAutoReconnectCookie(Some(..)) => f.write_str("SetAutoReconnectCookie(Some(..))"),
Expand Down Expand Up @@ -1347,6 +1353,11 @@ impl RdpServer {
clippy::too_many_arguments,
reason = "called via the builder; positional parameters are an internal detail"
)]
#[expect(
clippy::similar_names,
reason = "rdpei (MS-RDPEI touch/pen) and rdpeai (MS-RDPEAI audio input) are distinct protocols \
whose names happen to be textually close; renaming either would be less accurate"
)]
pub(crate) fn new(
opts: RdpServerOptions,
handler: Box<dyn RdpServerInputHandler>,
Expand All @@ -1356,6 +1367,7 @@ impl RdpServer {
mut cliprdr_factory: Option<Box<dyn CliprdrServerFactory>>,
mut rdpei_factory: Option<Box<dyn RdpeiServerFactory>>,
mut rdpdr_factory: Option<Box<dyn RdpdrServerFactory>>,
mut rdpeai_factory: Option<Box<dyn RdpeaiServerFactory>>,
connection_handler: Option<Box<dyn ConnectionHandler>>,
#[cfg(feature = "egfx")] mut gfx_factory: Option<Box<dyn GfxServerFactory>>,
display_suppressed: Option<Arc<AtomicBool>>,
Expand All @@ -1377,6 +1389,9 @@ impl RdpServer {
if let Some(rdpdr) = rdpdr_factory.as_mut() {
rdpdr.set_sender(ev_sender.clone());
}
if let Some(rdpeai) = rdpeai_factory.as_mut() {
rdpeai.set_sender(ev_sender.clone());
}
#[cfg(feature = "egfx")]
if let Some(gfx) = gfx_factory.as_mut() {
gfx.set_sender(ev_sender.clone());
Expand All @@ -1392,6 +1407,7 @@ impl RdpServer {
cliprdr_factory,
rdpei_factory,
rdpdr_factory,
rdpeai_factory,
echo_handle: EchoServerHandle::new(ev_sender.clone()),
#[cfg(feature = "egfx")]
gfx_factory,
Expand Down Expand Up @@ -1853,6 +1869,13 @@ impl RdpServer {
dvc
};

let dvc = if let Some(factory) = self.rdpeai_factory.as_deref() {
let backend = factory.build_backend();
dvc.with_dynamic_channel(RdpeaiServer::new(backend))
} else {
dvc
};

#[cfg(feature = "egfx")]
let dvc = {
let mut dvc = dvc;
Expand Down Expand Up @@ -2876,6 +2899,62 @@ impl RdpServer {
.await
.map_err(|e| ServerError::io("write_all", e))?;
}
ServerEvent::Rdpeai(msg) => {
let Some(drdynvc) = self.get_svc_processor::<dvc::DrdynvcServer>() else {
warn!("No drdynvc channel, dropping AUDIO_INPUT event");
continue;
};
let Some(channel_id) = drdynvc.get_channel_id_by_type::<RdpeaiServer>() else {
warn!("No AUDIO_INPUT dynamic channel, dropping event");
continue;
};
// dvc_by_id_mut already returns None for a channel that isn't yet
// Opened, and that case is handled just below; a separate
// is_channel_opened check here would only duplicate it.
let Some(mut rdpeai) = drdynvc.dvc_by_id_mut::<RdpeaiServer>(channel_id) else {
warn!("AUDIO_INPUT channel not opened or not found by id, dropping event");
continue;
};
let result = match msg {
RdpeaiServerMessage::Open {
frames_per_packet,
initial_format,
capture_format,
} => rdpeai
.processor_mut()
.open(frames_per_packet, initial_format, capture_format),
RdpeaiServerMessage::ChangeFormat { new_format } => {
rdpeai.processor_mut().change_format(new_format)
}
RdpeaiServerMessage::Error(error) => {
error!(?error, "Handling AUDIO_INPUT event");
continue;
}
};
// open()/change_format() reject calls that race the channel's current
// state (e.g. Open before negotiation finishes); that is an expected,
// recoverable condition per their own doc comments, not a connection
// fault, so drop-and-warn like every other unavailability in this arm
// rather than tearing down the session.
let msgs = match result {
Ok(msgs) => msgs,
Err(error) => {
warn!(%error, "AUDIO_INPUT event rejected by current channel state, dropping");
continue;
}
};
let dvc_messages = dvc::encode_dvc_messages(channel_id, msgs, ChannelFlags::SHOW_PROTOCOL)
.map_err(ServerError::encode)?;
let drdynvc_channel_id = self
.get_channel_id_by_type::<dvc::DrdynvcServer>()
.ok_or_else(|| ServerError::channel("DRDYNVC channel not found"))?;
let data = server_encode_svc_messages(dvc_messages, drdynvc_channel_id, user_channel_id)
.map_err(ServerError::encode)?;
writer
.write_all(&data)
.await
.map_err(|e| ServerError::io("write_all", e))?;
}
Comment thread
glamberson marked this conversation as resolved.
ServerEvent::Clipboard(c) => {
let Some(cliprdr) = self.get_svc_processor::<CliprdrServer>() else {
warn!("No clipboard channel, dropping event");
Expand Down
Loading
Loading