mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-08-04 07:08:17 +00:00
feat(session): graceful disconnection support (#336)
This commit is contained in:
parent
d24000a2ae
commit
056ec6a034
11 changed files with 236 additions and 57 deletions
|
@ -69,7 +69,10 @@ impl GuiContext {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
control_flow.set_exit();
|
if input_event_sender.send(RdpInputEvent::Close).is_err() {
|
||||||
|
error!("Failed to send graceful shutdown event, closing the window");
|
||||||
|
control_flow.set_exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::DroppedFile(_) => {
|
WindowEvent::DroppedFile(_) => {
|
||||||
// TODO(#110): File upload
|
// TODO(#110): File upload
|
||||||
|
@ -222,8 +225,8 @@ impl GuiContext {
|
||||||
}
|
}
|
||||||
Event::UserEvent(RdpOutputEvent::Terminated(result)) => {
|
Event::UserEvent(RdpOutputEvent::Terminated(result)) => {
|
||||||
let exit_code = match result {
|
let exit_code = match result {
|
||||||
Ok(()) => {
|
Ok(reason) => {
|
||||||
println!("Terminated gracefully");
|
println!("Terminated gracefully: {reason}");
|
||||||
proc_exit::sysexits::OK
|
proc_exit::sysexits::OK
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use ironrdp::connector::{ConnectionResult, ConnectorResult};
|
||||||
use ironrdp::graphics::image_processing::PixelFormat;
|
use ironrdp::graphics::image_processing::PixelFormat;
|
||||||
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
||||||
use ironrdp::session::image::DecodedImage;
|
use ironrdp::session::image::DecodedImage;
|
||||||
use ironrdp::session::{ActiveStage, ActiveStageOutput, SessionResult};
|
use ironrdp::session::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason, SessionResult};
|
||||||
use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session};
|
use ironrdp::{cliprdr, connector, rdpdr, rdpsnd, session};
|
||||||
use rdpdr::NoopRdpdrBackend;
|
use rdpdr::NoopRdpdrBackend;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -20,7 +20,7 @@ pub enum RdpOutputEvent {
|
||||||
PointerDefault,
|
PointerDefault,
|
||||||
PointerHidden,
|
PointerHidden,
|
||||||
PointerPosition { x: u16, y: u16 },
|
PointerPosition { x: u16, y: u16 },
|
||||||
Terminated(SessionResult<()>),
|
Terminated(SessionResult<GracefulDisconnectReason>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -67,8 +67,8 @@ impl RdpClient {
|
||||||
self.config.connector.desktop_size.width = width;
|
self.config.connector.desktop_size.width = width;
|
||||||
self.config.connector.desktop_size.height = height;
|
self.config.connector.desktop_size.height = height;
|
||||||
}
|
}
|
||||||
Ok(RdpControlFlow::TerminatedGracefully) => {
|
Ok(RdpControlFlow::TerminatedGracefully(reason)) => {
|
||||||
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(())));
|
let _ = self.event_loop_proxy.send_event(RdpOutputEvent::Terminated(Ok(reason)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -82,7 +82,7 @@ impl RdpClient {
|
||||||
|
|
||||||
enum RdpControlFlow {
|
enum RdpControlFlow {
|
||||||
ReconnectWithNewSize { width: u16, height: u16 },
|
ReconnectWithNewSize { width: u16, height: u16 },
|
||||||
TerminatedGracefully,
|
TerminatedGracefully(GracefulDisconnectReason),
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpgradedFramed = ironrdp_tokio::TokioFramed<ironrdp_tls::TlsStream<TcpStream>>;
|
type UpgradedFramed = ironrdp_tokio::TokioFramed<ironrdp_tls::TlsStream<TcpStream>>;
|
||||||
|
@ -162,7 +162,7 @@ async fn active_session(
|
||||||
|
|
||||||
let mut active_stage = ActiveStage::new(connection_result, None);
|
let mut active_stage = ActiveStage::new(connection_result, None);
|
||||||
|
|
||||||
'outer: loop {
|
let disconnect_reason = 'outer: loop {
|
||||||
let outputs = tokio::select! {
|
let outputs = tokio::select! {
|
||||||
frame = framed.read_pdu() => {
|
frame = framed.read_pdu() => {
|
||||||
let (action, payload) = frame.map_err(|e| session::custom_err!("read frame", e))?;
|
let (action, payload) = frame.map_err(|e| session::custom_err!("read frame", e))?;
|
||||||
|
@ -198,8 +198,7 @@ async fn active_session(
|
||||||
active_stage.process_fastpath_input(&mut image, &events)?
|
active_stage.process_fastpath_input(&mut image, &events)?
|
||||||
}
|
}
|
||||||
RdpInputEvent::Close => {
|
RdpInputEvent::Close => {
|
||||||
// TODO(#115): https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
active_stage.graceful_shutdown()?
|
||||||
break 'outer;
|
|
||||||
}
|
}
|
||||||
RdpInputEvent::Clipboard(event) => {
|
RdpInputEvent::Clipboard(event) => {
|
||||||
if let Some(cliprdr) = active_stage.get_svc_processor::<ironrdp::cliprdr::Cliprdr>() {
|
if let Some(cliprdr) = active_stage.get_svc_processor::<ironrdp::cliprdr::Cliprdr>() {
|
||||||
|
@ -281,10 +280,10 @@ async fn active_session(
|
||||||
ActiveStageOutput::PointerBitmap(_) => {
|
ActiveStageOutput::PointerBitmap(_) => {
|
||||||
// Not applicable, because we use the software cursor rendering.
|
// Not applicable, because we use the software cursor rendering.
|
||||||
}
|
}
|
||||||
ActiveStageOutput::Terminate => break 'outer,
|
ActiveStageOutput::Terminate(reason) => break 'outer reason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(RdpControlFlow::TerminatedGracefully)
|
Ok(RdpControlFlow::TerminatedGracefully(disconnect_reason))
|
||||||
}
|
}
|
||||||
|
|
|
@ -736,6 +736,22 @@ impl DisconnectReason {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn description(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::DomainDisconnected => "domain disconnected",
|
||||||
|
Self::ProviderInitiated => "server-initiated disconnect",
|
||||||
|
Self::TokenPurged => "token purged",
|
||||||
|
Self::UserRequested => "user-requested disconnect",
|
||||||
|
Self::ChannelPurged => "channel purged",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for DisconnectReason {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.write_str(self.description())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
@ -749,6 +765,10 @@ impl DisconnectProviderUltimatum {
|
||||||
pub const NAME: &'static str = "DisconnectProviderUltimatum";
|
pub const NAME: &'static str = "DisconnectProviderUltimatum";
|
||||||
|
|
||||||
pub const FIXED_PART_SIZE: usize = 2;
|
pub const FIXED_PART_SIZE: usize = 2;
|
||||||
|
|
||||||
|
pub fn from_reason(reason: DisconnectReason) -> Self {
|
||||||
|
Self { reason }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> McsPdu<'de> for DisconnectProviderUltimatum {
|
impl<'de> McsPdu<'de> for DisconnectProviderUltimatum {
|
||||||
|
|
|
@ -265,6 +265,7 @@ pub enum ShareDataPdu {
|
||||||
ServerSetErrorInfo(ServerSetErrorInfoPdu),
|
ServerSetErrorInfo(ServerSetErrorInfoPdu),
|
||||||
Input(InputEventPdu),
|
Input(InputEventPdu),
|
||||||
ShutdownRequest,
|
ShutdownRequest,
|
||||||
|
ShutdownDenied,
|
||||||
SuppressOutput(SuppressOutputPdu),
|
SuppressOutput(SuppressOutputPdu),
|
||||||
RefreshRectangle(RefreshRectanglePdu),
|
RefreshRectangle(RefreshRectanglePdu),
|
||||||
}
|
}
|
||||||
|
@ -281,7 +282,8 @@ impl ShareDataPdu {
|
||||||
ShareDataPdu::FrameAcknowledge(_) => "Frame Acknowledge PDU",
|
ShareDataPdu::FrameAcknowledge(_) => "Frame Acknowledge PDU",
|
||||||
ShareDataPdu::ServerSetErrorInfo(_) => "Server Set Error Info PDU",
|
ShareDataPdu::ServerSetErrorInfo(_) => "Server Set Error Info PDU",
|
||||||
ShareDataPdu::Input(_) => "Server Input PDU",
|
ShareDataPdu::Input(_) => "Server Input PDU",
|
||||||
ShareDataPdu::ShutdownRequest => "Shutdown Request",
|
ShareDataPdu::ShutdownRequest => "Shutdown Request PDU",
|
||||||
|
ShareDataPdu::ShutdownDenied => "Shutdown Denied PDU",
|
||||||
ShareDataPdu::SuppressOutput(_) => "Suppress Output PDU",
|
ShareDataPdu::SuppressOutput(_) => "Suppress Output PDU",
|
||||||
ShareDataPdu::RefreshRectangle(_) => "Refresh Rectangle PDU",
|
ShareDataPdu::RefreshRectangle(_) => "Refresh Rectangle PDU",
|
||||||
}
|
}
|
||||||
|
@ -309,6 +311,7 @@ impl ShareDataPdu {
|
||||||
)),
|
)),
|
||||||
ShareDataPduType::Input => Ok(ShareDataPdu::Input(InputEventPdu::from_buffer(&mut stream)?)),
|
ShareDataPduType::Input => Ok(ShareDataPdu::Input(InputEventPdu::from_buffer(&mut stream)?)),
|
||||||
ShareDataPduType::ShutdownRequest => Ok(ShareDataPdu::ShutdownRequest),
|
ShareDataPduType::ShutdownRequest => Ok(ShareDataPdu::ShutdownRequest),
|
||||||
|
ShareDataPduType::ShutdownDenied => Ok(ShareDataPdu::ShutdownDenied),
|
||||||
ShareDataPduType::SuppressOutput => Ok(ShareDataPdu::SuppressOutput(SuppressOutputPdu::from_buffer(
|
ShareDataPduType::SuppressOutput => Ok(ShareDataPdu::SuppressOutput(SuppressOutputPdu::from_buffer(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
)?)),
|
)?)),
|
||||||
|
@ -318,7 +321,6 @@ impl ShareDataPdu {
|
||||||
ShareDataPduType::Update
|
ShareDataPduType::Update
|
||||||
| ShareDataPduType::Pointer
|
| ShareDataPduType::Pointer
|
||||||
| ShareDataPduType::PlaySound
|
| ShareDataPduType::PlaySound
|
||||||
| ShareDataPduType::ShutdownDenied
|
|
||||||
| ShareDataPduType::SetKeyboardIndicators
|
| ShareDataPduType::SetKeyboardIndicators
|
||||||
| ShareDataPduType::BitmapCachePersistentList
|
| ShareDataPduType::BitmapCachePersistentList
|
||||||
| ShareDataPduType::BitmapCacheErrorPdu
|
| ShareDataPduType::BitmapCacheErrorPdu
|
||||||
|
@ -343,7 +345,7 @@ impl ShareDataPdu {
|
||||||
ShareDataPdu::FrameAcknowledge(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
ShareDataPdu::FrameAcknowledge(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
||||||
ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
||||||
ShareDataPdu::Input(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
ShareDataPdu::Input(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
||||||
ShareDataPdu::ShutdownRequest => Ok(()),
|
ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => Ok(()),
|
||||||
ShareDataPdu::SuppressOutput(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
ShareDataPdu::SuppressOutput(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
||||||
ShareDataPdu::RefreshRectangle(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
ShareDataPdu::RefreshRectangle(pdu) => pdu.to_buffer(&mut stream).map_err(RdpError::from),
|
||||||
}
|
}
|
||||||
|
@ -359,7 +361,7 @@ impl ShareDataPdu {
|
||||||
ShareDataPdu::FrameAcknowledge(pdu) => pdu.buffer_length(),
|
ShareDataPdu::FrameAcknowledge(pdu) => pdu.buffer_length(),
|
||||||
ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.buffer_length(),
|
ShareDataPdu::ServerSetErrorInfo(pdu) => pdu.buffer_length(),
|
||||||
ShareDataPdu::Input(pdu) => pdu.buffer_length(),
|
ShareDataPdu::Input(pdu) => pdu.buffer_length(),
|
||||||
ShareDataPdu::ShutdownRequest => 0,
|
ShareDataPdu::ShutdownRequest | ShareDataPdu::ShutdownDenied => 0,
|
||||||
ShareDataPdu::SuppressOutput(pdu) => pdu.buffer_length(),
|
ShareDataPdu::SuppressOutput(pdu) => pdu.buffer_length(),
|
||||||
ShareDataPdu::RefreshRectangle(pdu) => pdu.buffer_length(),
|
ShareDataPdu::RefreshRectangle(pdu) => pdu.buffer_length(),
|
||||||
}
|
}
|
||||||
|
@ -376,6 +378,7 @@ impl ShareDataPdu {
|
||||||
ShareDataPdu::ServerSetErrorInfo(_) => ShareDataPduType::SetErrorInfoPdu,
|
ShareDataPdu::ServerSetErrorInfo(_) => ShareDataPduType::SetErrorInfoPdu,
|
||||||
ShareDataPdu::Input(_) => ShareDataPduType::Input,
|
ShareDataPdu::Input(_) => ShareDataPduType::Input,
|
||||||
ShareDataPdu::ShutdownRequest => ShareDataPduType::ShutdownRequest,
|
ShareDataPdu::ShutdownRequest => ShareDataPduType::ShutdownRequest,
|
||||||
|
ShareDataPdu::ShutdownDenied => ShareDataPduType::ShutdownDenied,
|
||||||
ShareDataPdu::SuppressOutput(_) => ShareDataPduType::SuppressOutput,
|
ShareDataPdu::SuppressOutput(_) => ShareDataPduType::SuppressOutput,
|
||||||
ShareDataPdu::RefreshRectangle(_) => ShareDataPduType::RefreshRectangle,
|
ShareDataPdu::RefreshRectangle(_) => ShareDataPduType::RefreshRectangle,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,15 @@ use ironrdp_connector::ConnectionResult;
|
||||||
use ironrdp_graphics::pointer::DecodedPointer;
|
use ironrdp_graphics::pointer::DecodedPointer;
|
||||||
use ironrdp_pdu::geometry::InclusiveRectangle;
|
use ironrdp_pdu::geometry::InclusiveRectangle;
|
||||||
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
|
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
|
||||||
|
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||||
use ironrdp_pdu::write_buf::WriteBuf;
|
use ironrdp_pdu::write_buf::WriteBuf;
|
||||||
use ironrdp_pdu::{Action, PduParsing};
|
use ironrdp_pdu::{mcs, Action, PduParsing};
|
||||||
use ironrdp_svc::{StaticVirtualChannelProcessor, SvcProcessorMessages};
|
use ironrdp_svc::{StaticVirtualChannelProcessor, SvcProcessorMessages};
|
||||||
|
|
||||||
use crate::fast_path::UpdateKind;
|
use crate::fast_path::UpdateKind;
|
||||||
use crate::image::DecodedImage;
|
use crate::image::DecodedImage;
|
||||||
use crate::x224::GfxHandler;
|
use crate::x224::GfxHandler;
|
||||||
use crate::{fast_path, x224, SessionResult};
|
use crate::{fast_path, x224, SessionError, SessionResult};
|
||||||
|
|
||||||
pub struct ActiveStage {
|
pub struct ActiveStage {
|
||||||
x224_processor: x224::Processor,
|
x224_processor: x224::Processor,
|
||||||
|
@ -105,21 +106,26 @@ impl ActiveStage {
|
||||||
action: Action,
|
action: Action,
|
||||||
frame: &[u8],
|
frame: &[u8],
|
||||||
) -> SessionResult<Vec<ActiveStageOutput>> {
|
) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||||
let (output, processor_updates) = match action {
|
let (mut stage_outputs, processor_updates) = match action {
|
||||||
Action::FastPath => {
|
Action::FastPath => {
|
||||||
let mut output = WriteBuf::new();
|
let mut output = WriteBuf::new();
|
||||||
let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?;
|
let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?;
|
||||||
(output.into_inner(), processor_updates)
|
(
|
||||||
|
vec![ActiveStageOutput::ResponseFrame(output.into_inner())],
|
||||||
|
processor_updates,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Action::X224 => {
|
||||||
|
let outputs = self
|
||||||
|
.x224_processor
|
||||||
|
.process(frame)?
|
||||||
|
.into_iter()
|
||||||
|
.map(TryFrom::try_from)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
(outputs, Vec::new())
|
||||||
}
|
}
|
||||||
Action::X224 => (self.x224_processor.process(frame)?, Vec::new()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut stage_outputs = Vec::new();
|
|
||||||
|
|
||||||
if !output.is_empty() {
|
|
||||||
stage_outputs.push(ActiveStageOutput::ResponseFrame(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
for update in processor_updates {
|
for update in processor_updates {
|
||||||
match update {
|
match update {
|
||||||
UpdateKind::None => {}
|
UpdateKind::None => {}
|
||||||
|
@ -144,17 +150,27 @@ impl ActiveStage {
|
||||||
Ok(stage_outputs)
|
Ok(stage_outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encodes client-side graceful shutdown request. Note that upon sending this request,
|
||||||
|
/// client should wait for server's ShutdownDenied PDU before closing the connection.
|
||||||
|
///
|
||||||
|
/// Client-side graceful shutdown is defined in [MS-RDPBCGR]
|
||||||
|
///
|
||||||
|
/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||||
|
pub fn graceful_shutdown(&self) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||||
|
let mut frame = WriteBuf::new();
|
||||||
|
self.x224_processor
|
||||||
|
.encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?;
|
||||||
|
|
||||||
|
Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())])
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends a PDU on the dynamic channel.
|
/// Sends a PDU on the dynamic channel.
|
||||||
pub fn encode_dynamic(&self, output: &mut WriteBuf, channel_name: &str, dvc_data: &[u8]) -> SessionResult<()> {
|
pub fn encode_dynamic(&self, output: &mut WriteBuf, channel_name: &str, dvc_data: &[u8]) -> SessionResult<()> {
|
||||||
self.x224_processor.encode_dynamic(output, channel_name, dvc_data)
|
self.x224_processor.encode_dynamic(output, channel_name, dvc_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a pdu on the static global channel. Typically used to send input events
|
/// Send a pdu on the static global channel. Typically used to send input events
|
||||||
pub fn encode_static(
|
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
|
||||||
&self,
|
|
||||||
output: &mut WriteBuf,
|
|
||||||
pdu: ironrdp_pdu::rdp::headers::ShareDataPdu,
|
|
||||||
) -> SessionResult<usize> {
|
|
||||||
self.x224_processor.encode_static(output, pdu)
|
self.x224_processor.encode_static(output, pdu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,5 +200,49 @@ pub enum ActiveStageOutput {
|
||||||
PointerHidden,
|
PointerHidden,
|
||||||
PointerPosition { x: u16, y: u16 },
|
PointerPosition { x: u16, y: u16 },
|
||||||
PointerBitmap(Rc<DecodedPointer>),
|
PointerBitmap(Rc<DecodedPointer>),
|
||||||
Terminate,
|
Terminate(GracefulDisconnectReason),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<x224::ProcessorOutput> for ActiveStageOutput {
|
||||||
|
type Error = SessionError;
|
||||||
|
|
||||||
|
fn try_from(value: x224::ProcessorOutput) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)),
|
||||||
|
x224::ProcessorOutput::Disconnect(reason) => {
|
||||||
|
let reason = match reason {
|
||||||
|
mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated,
|
||||||
|
mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated,
|
||||||
|
other => GracefulDisconnectReason::Other(other.description()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::Terminate(reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for
|
||||||
|
/// disconnect reasons.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum GracefulDisconnectReason {
|
||||||
|
UserInitiated,
|
||||||
|
ServerInitiated,
|
||||||
|
Other(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GracefulDisconnectReason {
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
GracefulDisconnectReason::UserInitiated => "user initiated disconnect",
|
||||||
|
GracefulDisconnectReason::ServerInitiated => "server initiated disconnect",
|
||||||
|
GracefulDisconnectReason::Other(description) => description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for GracefulDisconnectReason {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.write_str(self.description())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ mod active_stage;
|
||||||
|
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
pub use active_stage::{ActiveStage, ActiveStageOutput};
|
pub use active_stage::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason};
|
||||||
|
|
||||||
pub type SessionResult<T> = Result<T, SessionError>;
|
pub type SessionResult<T> = Result<T, SessionError>;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::collections::HashMap;
|
||||||
use ironrdp_connector::legacy::SendDataIndicationCtx;
|
use ironrdp_connector::legacy::SendDataIndicationCtx;
|
||||||
use ironrdp_connector::GraphicsConfig;
|
use ironrdp_connector::GraphicsConfig;
|
||||||
use ironrdp_pdu::dvc::FieldType;
|
use ironrdp_pdu::dvc::FieldType;
|
||||||
|
use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage};
|
||||||
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||||
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
|
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
|
||||||
use ironrdp_pdu::rdp::vc::dvc;
|
use ironrdp_pdu::rdp::vc::dvc;
|
||||||
|
@ -17,7 +18,7 @@ use ironrdp_svc::{
|
||||||
StaticChannelSet, StaticVirtualChannel, StaticVirtualChannelProcessor, SvcMessage, SvcProcessorMessages,
|
StaticChannelSet, StaticVirtualChannel, StaticVirtualChannelProcessor, SvcMessage, SvcProcessorMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{SessionErrorExt as _, SessionResult};
|
use crate::{SessionError, SessionErrorExt as _, SessionResult};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub use self::gfx::GfxHandler;
|
pub use self::gfx::GfxHandler;
|
||||||
|
@ -25,6 +26,15 @@ pub use self::gfx::GfxHandler;
|
||||||
pub const RDP8_GRAPHICS_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::Graphics";
|
pub const RDP8_GRAPHICS_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::Graphics";
|
||||||
pub const RDP8_DISPLAY_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::DisplayControl";
|
pub const RDP8_DISPLAY_PIPELINE_NAME: &str = "Microsoft::Windows::RDS::DisplayControl";
|
||||||
|
|
||||||
|
/// X224 Processor output
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ProcessorOutput {
|
||||||
|
/// A buffer with encoded data to send to the server.
|
||||||
|
ResponseFrame(Vec<u8>),
|
||||||
|
/// A graceful disconnect notification. Client should close the connection upon receiving this.
|
||||||
|
Disconnect(DisconnectReason),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Processor {
|
pub struct Processor {
|
||||||
channel_map: HashMap<String, u32>,
|
channel_map: HashMap<String, u32>,
|
||||||
static_channels: StaticChannelSet,
|
static_channels: StaticChannelSet,
|
||||||
|
@ -90,26 +100,28 @@ impl Processor {
|
||||||
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
|
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a received PDU. Returns a buffer with encoded data to send to the server, if any.
|
/// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed
|
||||||
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<u8>> {
|
/// in the returned order.
|
||||||
|
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<ProcessorOutput>> {
|
||||||
let data_ctx: SendDataIndicationCtx<'_> =
|
let data_ctx: SendDataIndicationCtx<'_> =
|
||||||
ironrdp_connector::legacy::decode_send_data_indication(frame).map_err(crate::legacy::map_error)?;
|
ironrdp_connector::legacy::decode_send_data_indication(frame).map_err(crate::legacy::map_error)?;
|
||||||
let channel_id = data_ctx.channel_id;
|
let channel_id = data_ctx.channel_id;
|
||||||
|
|
||||||
if channel_id == self.io_channel_id {
|
if channel_id == self.io_channel_id {
|
||||||
self.process_io_channel(data_ctx)?;
|
self.process_io_channel(data_ctx)
|
||||||
Ok(Vec::new())
|
|
||||||
} else if self.drdynvc_channel_id == Some(channel_id) {
|
} else if self.drdynvc_channel_id == Some(channel_id) {
|
||||||
self.process_dyvc(data_ctx)
|
self.process_dyvc(data_ctx)
|
||||||
|
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
|
||||||
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
|
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
|
||||||
let response_pdus = svc.process(data_ctx.user_data).map_err(crate::SessionError::pdu)?;
|
let response_pdus = svc.process(data_ctx.user_data).map_err(crate::SessionError::pdu)?;
|
||||||
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
|
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
|
||||||
|
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
|
||||||
} else {
|
} else {
|
||||||
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
|
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<()> {
|
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
|
||||||
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
|
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
|
||||||
|
|
||||||
let ctx = ironrdp_connector::legacy::decode_share_data(data_ctx).map_err(crate::legacy::map_error)?;
|
let ctx = ironrdp_connector::legacy::decode_share_data(data_ctx).map_err(crate::legacy::map_error)?;
|
||||||
|
@ -117,16 +129,47 @@ impl Processor {
|
||||||
match ctx.pdu {
|
match ctx.pdu {
|
||||||
ShareDataPdu::SaveSessionInfo(session_info) => {
|
ShareDataPdu::SaveSessionInfo(session_info) => {
|
||||||
debug!("Got Session Save Info PDU: {session_info:?}");
|
debug!("Got Session Save Info PDU: {session_info:?}");
|
||||||
Ok(())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
|
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
|
||||||
ProtocolIndependentCode::None,
|
ProtocolIndependentCode::None,
|
||||||
))) => {
|
))) => {
|
||||||
debug!("Received None server error");
|
debug!("Received None server error");
|
||||||
Ok(())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
|
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
|
||||||
Err(reason_err!("ServerSetErrorInfo", "{}", e.description()))
|
// This is a part of server-side graceful disconnect procedure defined
|
||||||
|
// in [MS-RDPBCGR].
|
||||||
|
//
|
||||||
|
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8
|
||||||
|
let graceful_disconnect = error_info_to_graceful_disconnect_reason(&e);
|
||||||
|
|
||||||
|
if let Some(reason) = graceful_disconnect {
|
||||||
|
debug!("Received server-side graceful disconnect request: {reason}");
|
||||||
|
|
||||||
|
Ok(vec![ProcessorOutput::Disconnect(reason)])
|
||||||
|
} else {
|
||||||
|
Err(reason_err!("ServerSetErrorInfo", "{}", e.description()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShareDataPdu::ShutdownDenied => {
|
||||||
|
debug!("ShutdownDenied received, session will be closed");
|
||||||
|
|
||||||
|
// As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we
|
||||||
|
// need to send a disconnect ultimatum to the server if we want to proceed with the
|
||||||
|
// session shutdown.
|
||||||
|
//
|
||||||
|
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||||
|
let ultimatum = McsMessage::DisconnectProviderUltimatum(DisconnectProviderUltimatum::from_reason(
|
||||||
|
DisconnectReason::UserRequested,
|
||||||
|
));
|
||||||
|
|
||||||
|
let encoded_pdu = ironrdp_pdu::encode_vec(&ultimatum).map_err(SessionError::pdu);
|
||||||
|
|
||||||
|
Ok(vec![
|
||||||
|
ProcessorOutput::ResponseFrame(encoded_pdu?),
|
||||||
|
ProcessorOutput::Disconnect(DisconnectReason::UserRequested),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
_ => Err(reason_err!(
|
_ => Err(reason_err!(
|
||||||
"IO channel",
|
"IO channel",
|
||||||
|
@ -507,3 +550,24 @@ impl CompleteData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an [`ErrorInfo`] into a [`DisconnectReason`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if the error code is not a graceful disconnect code.
|
||||||
|
pub fn error_info_to_graceful_disconnect_reason(error_info: &ErrorInfo) -> Option<DisconnectReason> {
|
||||||
|
let code = if let ErrorInfo::ProtocolIndependentCode(code) = error_info {
|
||||||
|
code
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
match code {
|
||||||
|
ProtocolIndependentCode::RpcInitiatedDisconnect
|
||||||
|
| ProtocolIndependentCode::RpcInitiatedLogoff
|
||||||
|
| ProtocolIndependentCode::DisconnectedByOtherconnection => Some(DisconnectReason::ProviderInitiated),
|
||||||
|
ProtocolIndependentCode::RpcInitiatedDisconnectByuser | ProtocolIndependentCode::LogoffByUser => {
|
||||||
|
Some(DisconnectReason::UserRequested)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ use ironrdp::graphics::image_processing::PixelFormat;
|
||||||
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
||||||
use ironrdp::pdu::write_buf::WriteBuf;
|
use ironrdp::pdu::write_buf::WriteBuf;
|
||||||
use ironrdp::session::image::DecodedImage;
|
use ironrdp::session::image::DecodedImage;
|
||||||
use ironrdp::session::{ActiveStage, ActiveStageOutput};
|
use ironrdp::session::{ActiveStage, ActiveStageOutput, GracefulDisconnectReason};
|
||||||
use rgb::AsPixels as _;
|
use rgb::AsPixels as _;
|
||||||
use tap::prelude::*;
|
use tap::prelude::*;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
@ -342,6 +342,7 @@ pub(crate) enum RdpInputEvent {
|
||||||
Cliprdr(ClipboardMessage),
|
Cliprdr(ClipboardMessage),
|
||||||
ClipboardBackend(WasmClipboardBackendMessage),
|
ClipboardBackend(WasmClipboardBackendMessage),
|
||||||
FastPath(FastPathInputEvents),
|
FastPath(FastPathInputEvents),
|
||||||
|
TerminateSession,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CursorStyle {
|
enum CursorStyle {
|
||||||
|
@ -354,6 +355,18 @@ enum CursorStyle {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct SessionTerminationInfo {
|
||||||
|
reason: GracefulDisconnectReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl SessionTerminationInfo {
|
||||||
|
pub fn reason(&self) -> String {
|
||||||
|
self.reason.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
desktop_size: connector::DesktopSize,
|
desktop_size: connector::DesktopSize,
|
||||||
|
@ -374,7 +387,7 @@ pub struct Session {
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl Session {
|
impl Session {
|
||||||
pub async fn run(&self) -> Result<(), IronRdpError> {
|
pub async fn run(&self) -> Result<SessionTerminationInfo, IronRdpError> {
|
||||||
let rdp_reader = self
|
let rdp_reader = self
|
||||||
.rdp_reader
|
.rdp_reader
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -418,7 +431,7 @@ impl Session {
|
||||||
|
|
||||||
let mut active_stage = ActiveStage::new(connection_result, None);
|
let mut active_stage = ActiveStage::new(connection_result, None);
|
||||||
|
|
||||||
'outer: loop {
|
let disconnect_reason = 'outer: loop {
|
||||||
let outputs = select! {
|
let outputs = select! {
|
||||||
frame = framed.read_pdu().fuse() => {
|
frame = framed.read_pdu().fuse() => {
|
||||||
let (action, payload) = frame.context("read frame")?;
|
let (action, payload) = frame.context("read frame")?;
|
||||||
|
@ -471,7 +484,11 @@ impl Session {
|
||||||
}
|
}
|
||||||
RdpInputEvent::FastPath(events) => {
|
RdpInputEvent::FastPath(events) => {
|
||||||
active_stage.process_fastpath_input(&mut image, &events)
|
active_stage.process_fastpath_input(&mut image, &events)
|
||||||
.context("Fast path input events processing")?
|
.context("fast path input events processing")?
|
||||||
|
}
|
||||||
|
RdpInputEvent::TerminateSession => {
|
||||||
|
active_stage.graceful_shutdown()
|
||||||
|
.context("graceful shutdown")?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -589,14 +606,16 @@ impl Session {
|
||||||
hotspot_y,
|
hotspot_y,
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
ActiveStageOutput::Terminate => break 'outer,
|
ActiveStageOutput::Terminate(reason) => break 'outer reason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
info!("RPD session terminated");
|
info!(%disconnect_reason, "RPD session terminated");
|
||||||
|
|
||||||
Ok(())
|
Ok(SessionTerminationInfo {
|
||||||
|
reason: disconnect_reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn desktop_size(&self) -> DesktopSize {
|
pub fn desktop_size(&self) -> DesktopSize {
|
||||||
|
@ -651,9 +670,11 @@ impl Session {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unused_self)] // FIXME: not yet implemented
|
|
||||||
pub fn shutdown(&self) -> Result<(), IronRdpError> {
|
pub fn shutdown(&self) -> Result<(), IronRdpError> {
|
||||||
// TODO(#115): https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
self.input_events_tx
|
||||||
|
.unbounded_send(RdpInputEvent::TerminateSession)
|
||||||
|
.context("failed to send terminate session event to writer task")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -291,7 +291,7 @@ fn active_stage(
|
||||||
for out in outputs {
|
for out in outputs {
|
||||||
match out {
|
match out {
|
||||||
ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?,
|
ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?,
|
||||||
ActiveStageOutput::Terminate => break 'outer,
|
ActiveStageOutput::Terminate(_) => break 'outer,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import init, {
|
||||||
Session,
|
Session,
|
||||||
SessionBuilder,
|
SessionBuilder,
|
||||||
ClipboardTransaction,
|
ClipboardTransaction,
|
||||||
|
SessionTerminationInfo,
|
||||||
} from '../../../../crates/ironrdp-web/pkg/ironrdp_web';
|
} from '../../../../crates/ironrdp-web/pkg/ironrdp_web';
|
||||||
import { loggingService } from './logging.service';
|
import { loggingService } from './logging.service';
|
||||||
import { catchError, filter, map } from 'rxjs/operators';
|
import { catchError, filter, map } from 'rxjs/operators';
|
||||||
|
@ -188,6 +189,13 @@ export class WasmBridgeService {
|
||||||
});
|
});
|
||||||
return of(err);
|
return of(err);
|
||||||
}),
|
}),
|
||||||
|
map((termination_info: SessionTerminationInfo) => {
|
||||||
|
this.setVisibility(false);
|
||||||
|
this.raiseSessionEvent({
|
||||||
|
type: SessionEventType.TERMINATED,
|
||||||
|
data: 'Session was terminated: ' + termination_info.reason() + '.',
|
||||||
|
});
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
return session;
|
return session;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{local_bin, prelude::*};
|
use crate::local_bin;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn check(sh: &Shell) -> anyhow::Result<()> {
|
pub fn check(sh: &Shell) -> anyhow::Result<()> {
|
||||||
let _s = Section::new("WASM-CHECK");
|
let _s = Section::new("WASM-CHECK");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue