feat(blocking): initial implementation (#158)

This commit is contained in:
Benoît Cortier
2023-09-11 15:47:26 +00:00
committed by GitHub
parent 9d33cad303
commit babbd68af5
21 changed files with 835 additions and 88 deletions
+1
View File
@@ -46,6 +46,7 @@ a feature flag called `alloc` must exist to enable its use.
Higher level libraries and binaries built on top of the core tier.
Guidelines and constraints are relaxed to some extent.
- `crates/ironrdp-blocking`: blocking I/O abstraction wrapping the state machines conveniently.
- `crates/ironrdp-async`: provides `Future`s wrapping the state machines conveniently.
- `crates/ironrdp-tokio`: `Framed*` traits implementation above `tokio`s traits.
- `crates/ironrdp-futures`: `Framed*` traits implementation above `futures`s traits.
Generated
+20 -1
View File
@@ -993,7 +993,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.0",
]
[[package]]
@@ -1797,7 +1797,10 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
name = "ironrdp"
version = "0.5.0"
dependencies = [
"anyhow",
"bmp",
"ironrdp-acceptor",
"ironrdp-blocking",
"ironrdp-cliprdr",
"ironrdp-connector",
"ironrdp-dvc",
@@ -1809,6 +1812,11 @@ dependencies = [
"ironrdp-server",
"ironrdp-session",
"ironrdp-svc",
"pico-args",
"rustls",
"tracing",
"tracing-subscriber",
"x509-cert",
]
[[package]]
@@ -1832,6 +1840,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "ironrdp-blocking"
version = "0.1.0"
dependencies = [
"bytes",
"ironrdp-connector",
"ironrdp-pdu",
"tap",
"tracing",
]
[[package]]
name = "ironrdp-client"
version = "0.1.0"
+1
View File
@@ -23,6 +23,7 @@ categories = ["network-programming"]
[workspace.dependencies]
ironrdp-acceptor = { version = "0.1", path = "crates/ironrdp-acceptor" }
ironrdp-async = { version = "0.1", path = "crates/ironrdp-async" }
ironrdp-blocking = { version = "0.1", path = "crates/ironrdp-blocking" }
ironrdp-cliprdr = { version = "0.1", path = "crates/ironrdp-cliprdr" }
ironrdp-connector = { version = "0.1", path = "crates/ironrdp-connector" }
ironrdp-dvc = { version = "0.1", path = "crates/ironrdp-dvc" }
+26
View File
@@ -15,6 +15,32 @@ Supported codecs:
- RDP 6.0 Bitmap Compression
- Microsoft RemoteFX (RFX)
## Examples
### [`ironrdp-client`](./crates/ironrdp-client)
A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O.
```bash
cargo run --bin ironrdp-client -- <HOSTNAME> --username <USERNAME> --password <PASSWORD>
```
### [`screenshot`](./crates/ironrdp/examples/screenshot.rs)
Example of utilizing IronRDP in a blocking, synchronous fashion.
This example showcases the use of IronRDP in a blocking manner. It
demonstrates how to create a basic RDP client with just a few hundred lines
of code by leveraging the IronRDP crates suite.
In this basic client implementation, the client establishes a connection
with the destination server, decodes incoming graphics updates, and saves the
resulting output as a BMP image file on the local disk.
```bash
cargo run --example=screenshot -- --host <HOSTNAME> --username <USERNAME> --password <PASSWORD> --output out.bmp
```
### How to enable RemoteFX on server
Run the following PowerShell commands, and reboot.
+58 -6
View File
@@ -12,6 +12,12 @@ pub trait FramedRead {
Self: 'read;
/// Reads from stream and fills internal buffer
///
/// # Cancel safety
///
/// This method is cancel safe. If you use it as the event in a
/// `tokio::select!` statement and some other branch
/// completes first, then it is guaranteed that no data was read.
fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a>;
}
@@ -21,6 +27,14 @@ pub trait FramedWrite {
Self: 'write;
/// Writes an entire buffer into this stream.
///
/// # Cancel safety
///
/// This method is not cancellation safe. If it is used as the event
/// in a `tokio::select!` statement and some other
/// branch completes first, then the provided buffer may have been
/// partially written, but future calls to `write_all` will start over
/// from the beginning of the buffer.
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a>;
}
@@ -81,11 +95,14 @@ impl<S> Framed<S>
where
S: FramedRead,
{
/// Reads from stream and fills internal buffer
pub async fn read(&mut self) -> io::Result<usize> {
self.stream.read(&mut self.buf).await
}
/// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer.
///
/// # Cancel safety
///
/// This method is cancel safe. If you use it as the event in a
/// `tokio::select!` statement and some other branch
/// completes first, then it is safe to drop the future and re-create it later.
/// Data may have been read, but it will be stored in the internal buffer.
pub async fn read_exact(&mut self, length: usize) -> io::Result<BytesMut> {
loop {
if self.buf.len() >= length {
@@ -103,6 +120,14 @@ where
}
}
/// Reads a standard RDP PDU frame.
///
/// # Cancel safety
///
/// This method is cancel safe. If you use it as the event in a
/// `tokio::select!` statement and some other branch
/// completes first, then it is safe to drop the future and re-create it later.
/// Data may have been read, but it will be stored in the internal buffer.
pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> {
loop {
// Try decoding and see if a frame has been received already
@@ -125,6 +150,14 @@ where
}
}
/// Reads a frame using the provided PduHint.
///
/// # Cancel safety
///
/// This method is cancel safe. If you use it as the event in a
/// `tokio::select!` statement and some other branch
/// completes first, then it is safe to drop the future and re-create it later.
/// Data may have been read, but it will be stored in the internal buffer.
pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result<Bytes> {
loop {
match hint
@@ -145,13 +178,32 @@ where
};
}
}
/// Reads from stream and fills internal buffer, returning how many bytes were read.
///
/// # Cancel safety
///
/// This method is cancel safe. If you use it as the event in a
/// `tokio::select!` statement and some other branch
/// completes first, then it is guaranteed that no data was read.
async fn read(&mut self) -> io::Result<usize> {
self.stream.read(&mut self.buf).await
}
}
impl<S> Framed<S>
where
S: FramedWrite,
{
/// Writes an entire buffer into this stream.
/// Attempts to write an entire buffer into this `Framed`s stream.
///
/// # Cancel safety
///
/// This method is not cancellation safe. If it is used as the event
/// in a `tokio::select!` statement and some other
/// branch completes first, then the provided buffer may have been
/// partially written, but future calls to `write_all` will start over
/// from the beginning of the buffer.
pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.stream.write_all(buf).await
}
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "ironrdp-blocking"
version = "0.1.0"
readme = "README.md"
description = "Blocking I/O abstraction wrapping the IronRDP state machines conveniently"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[dependencies]
bytes = "1"
ironrdp-connector.workspace = true
ironrdp-pdu.workspace = true
# ironrdp-session.workspace = true
tap = "1"
tracing.workspace = true
+7
View File
@@ -0,0 +1,7 @@
# IronRDP Blocking
Blocking I/O abstraction wrapping the IronRDP state machines conveniently.
This crate is a higher level abstraction for IronRDP state machines using blocking I/O instead of
asynchronous I/O. This results in a simpler API with fewer dependencies that should be used
instead of `ironrdp-async` when concurrency is not a requirement.
+116
View File
@@ -0,0 +1,116 @@
use std::io::{Read, Write};
use ironrdp_connector::{
ClientConnector, ClientConnectorState, ConnectionResult, ConnectorResult, Sequence as _, State as _,
};
use ironrdp_pdu::write_buf::WriteBuf;
use crate::framed::Framed;
pub struct ShouldUpgrade {
_priv: (),
}
#[instrument(skip_all)]
pub fn connect_begin<S>(framed: &mut Framed<S>, connector: &mut ClientConnector) -> ConnectorResult<ShouldUpgrade>
where
S: Sync + Read + Write,
{
let mut buf = WriteBuf::new();
info!("Begin connection procedure");
while !connector.should_perform_security_upgrade() {
single_connect_step(framed, connector, &mut buf)?;
}
Ok(ShouldUpgrade { _priv: () })
}
pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade {
assert!(connector.should_perform_security_upgrade());
ShouldUpgrade { _priv: () }
}
pub struct Upgraded {
_priv: (),
}
#[instrument(skip_all)]
pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector, server_public_key: Vec<u8>) -> Upgraded {
trace!("marked as upgraded");
connector.attach_server_public_key(server_public_key);
connector.mark_security_upgrade_as_done();
Upgraded { _priv: () }
}
#[instrument(skip_all)]
pub fn connect_finalize<S>(
_: Upgraded,
framed: &mut Framed<S>,
mut connector: ClientConnector,
) -> ConnectorResult<ConnectionResult>
where
S: Read + Write,
{
let mut buf = WriteBuf::new();
debug!("CredSSP procedure");
while connector.is_credssp_step() {
single_connect_step(framed, &mut connector, &mut buf)?;
}
debug!("Remaining of connection sequence");
let result = loop {
single_connect_step(framed, &mut connector, &mut buf)?;
if let ClientConnectorState::Connected { result } = connector.state {
break result;
}
};
info!("Connected with success");
Ok(result)
}
pub fn single_connect_step<S>(
framed: &mut Framed<S>,
connector: &mut ClientConnector,
buf: &mut WriteBuf,
) -> ConnectorResult<ironrdp_connector::Written>
where
S: Read + Write,
{
buf.clear();
let written = if let Some(next_pdu_hint) = connector.next_pdu_hint() {
debug!(
connector.state = connector.state.name(),
hint = ?next_pdu_hint,
"Wait for PDU"
);
let pdu = framed
.read_by_hint(next_pdu_hint)
.map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?;
trace!(length = pdu.len(), "PDU received");
connector.step(&pdu, buf)?
} else {
connector.step_no_input(buf)?
};
if let Some(response_len) = written.size() {
let response = &buf[..response_len];
trace!(response_len, "Send response");
framed
.write_all(response)
.map_err(|e| ironrdp_connector::custom_err!("write all", e))?;
}
Ok(written)
}
+130
View File
@@ -0,0 +1,130 @@
use std::io::{self, Read, Write};
use bytes::{Bytes, BytesMut};
use ironrdp_pdu::PduHint;
pub struct Framed<S> {
stream: S,
buf: BytesMut,
}
impl<S> Framed<S> {
pub fn new(stream: S) -> Self {
Self {
stream,
buf: BytesMut::new(),
}
}
pub fn into_inner(self) -> (S, BytesMut) {
(self.stream, self.buf)
}
pub fn into_inner_no_leftover(self) -> S {
let (stream, leftover) = self.into_inner();
debug_assert_eq!(leftover.len(), 0, "unexpected leftover");
stream
}
pub fn get_inner(&self) -> (&S, &BytesMut) {
(&self.stream, &self.buf)
}
pub fn get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) {
(&mut self.stream, &mut self.buf)
}
pub fn peek(&self) -> &[u8] {
&self.buf
}
}
impl<S> Framed<S>
where
S: Read,
{
/// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer.
pub fn read_exact(&mut self, length: usize) -> io::Result<BytesMut> {
loop {
if self.buf.len() >= length {
return Ok(self.buf.split_to(length));
} else {
self.buf.reserve(length - self.buf.len());
}
let len = self.read()?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
}
/// Reads a standard RDP PDU frame.
pub fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> {
loop {
// Try decoding and see if a frame has been received already
match ironrdp_pdu::find_size(self.peek()) {
Ok(Some(pdu_info)) => {
let frame = self.read_exact(pdu_info.length)?;
return Ok((pdu_info.action, frame));
}
Ok(None) => {
let len = self.read()?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
};
}
}
/// Reads a frame using the provided PduHint.
pub fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result<Bytes> {
loop {
match hint
.find_size(self.peek())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
{
Some(length) => {
return Ok(self.read_exact(length)?.freeze());
}
None => {
let len = self.read()?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
};
}
}
/// Reads from stream and fills internal buffer, returning how many bytes were read.
fn read(&mut self) -> io::Result<usize> {
// FIXME(perf): use read_buf (https://doc.rust-lang.org/std/io/trait.Read.html#method.read_buf)
// once its stabilized. See tracking issue for RFC 2930: https://github.com/rust-lang/rust/issues/78485
let mut read_bytes = [0u8; 1024];
let len = self.stream.read(&mut read_bytes)?;
self.buf.extend_from_slice(&read_bytes[..len]);
Ok(len)
}
}
impl<S> Framed<S>
where
S: Write,
{
/// Attempts to write an entire buffer into this `Framed`s stream.
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.stream.write_all(buf)
}
}
+10
View File
@@ -0,0 +1,10 @@
#[macro_use]
extern crate tracing;
mod connector;
mod framed;
mod session;
pub use connector::*;
pub use framed::*;
pub use session::*;
+1
View File
@@ -0,0 +1 @@
// TODO: active session I/O helpers? Im not yet sure we need that
+1 -1
View File
@@ -52,7 +52,7 @@ tokio = { version = "1", features = ["full"]}
# Utils
chrono = "0.4"
whoami = "1.4"
anyhow = "1.0"
anyhow = "1"
smallvec = "1.10"
tap = "1"
semver = "1"
+33 -58
View File
@@ -1,66 +1,41 @@
# IronRDP client
A command-line RDP client, which performs connection to an RDP server and decodes RFX graphical updates.
If IronRDP client encounters an error, then will return `error` exit code and print what caused
an error.
A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O.
## Prerequisites
## Sample usage
You need to enable RemoteFX on the target machine (see top-level [README](../README.md)).
## Command-line Interface
```
USAGE:
ironrdp_client [OPTIONS] <ADDR> --password <PASSWORD> --security-protocol <SECURITY_PROTOCOL>... --username <USERNAME>
FLAGS:
-h, --help Prints help information
-v, --version Prints version information
OPTIONS:
--dig-product-id <DIG_PRODUCT_ID>
Contains a value that uniquely identifies the client [default: ]
-d, --domain <DOMAIN> An optional target RDP server domain name
--ime-file-name <IME_FILENAME>
The input method editor (IME) file name associated with the active input locale [default: ]
--keyboard-functional-keys-count <KEYBOARD_FUNCTIONAL_KEYS_COUNT>
The number of function keys on the keyboard [default: 12]
--keyboard-subtype <KEYBOARD_SUBTYPE>
The keyboard subtype (an original equipment manufacturer-dependent value) [default: 0]
--keyboard-type <KEYBOARD_TYPE>
The keyboard type [default: ibm_enhanced] [possible values: ibm_pc_xt, olivetti_ico, ibm_pc_at,
ibm_enhanced, nokia1050, nokia9140, japanese]
--log-file <LOG_FILE>
A file with IronRDP client logs [default: ironrdp_client.log]
-p, --password <PASSWORD> A target RDP server user password
--security-protocol <SECURITY_PROTOCOL>...
Specify the security protocols to use [default: hybrid_ex] [possible values: ssl, hybrid, hybrid_ex]
-u, --username <USERNAME> A target RDP server user name
ARGS:
<ADDR> An address on which the client will connect. Format: <ip>:<port>
```bash
ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
```
It worth to notice that the client takes mandatory arguments as
- `<ADDR>` as first argument;
- `--username` or `-u`;
- `--password` or `-p`.
## Configuring log filter directives
## Sample Usage
The `IRONRDP_LOG` environment variable is used to set the log filter directives.
```bash
IRONRDP_LOG="info,ironrdp_connector=trace" ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
```
See [`tracing-subscriber`s documentation][tracing-doc] for more details.
[tracing-doc]: https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/filter/struct.EnvFilter.html#directives
## Support for `SSLKEYLOGFILE`
This client supports reading the `SSLKEYLOGFILE` environment variable.
When set, the TLS encryption secrets for the session will be dumped to the file specified
by the environment variable.
This file can be read by Wireshark so that in can decrypt the packets.
### Example
```bash
SSLKEYLOGFILE=/tmp/tls-secrets ironrdp-client <HOSTNAME> --username <USERNAME> --password <PASSWORD>
```
### Usage in Wireshark
See this [awakecoding's repository][awakecoding-repository] explaining how to use the file in wireshark.
[awakecoding-repository]: https://github.com/awakecoding/wireshark-rdp#sslkeylogfile
1. Run the RDP server (Windows RDP server, FreeRDP server, etc.);
2. Run the IronRDP client and specify the RDP server address, username and password:
```
cargo run 192.168.1.100:3389 -u SimpleUsername -p SimplePassword!
```
3. After the RDP Connection Sequence the client will start receive RFX updates
and save to the internal buffer.
In case of error, the client will print (for example) `RDP failed because of negotiation error: ...`.
Additional logs are available in `<LOG_FILE>` (`ironrdp_client.log` by default).
+1 -1
View File
@@ -1,3 +1,4 @@
use ironrdp::connector::sspi::network_client::reqwest_network_client::RequestClientFactory;
use ironrdp::connector::{ConnectionResult, ConnectorResult};
use ironrdp::graphics::image_processing::PixelFormat;
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
@@ -5,7 +6,6 @@ use ironrdp::session::image::DecodedImage;
use ironrdp::session::{ActiveStage, ActiveStageOutput, SessionResult};
use ironrdp::{connector, session};
use smallvec::SmallVec;
use sspi::network_client::reqwest_network_client::RequestClientFactory;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use winit::event_loop::EventLoopProxy;
@@ -431,6 +431,8 @@ impl Sequence for ClientConnector {
let ts_request_from_server = credssp::TsRequest::from_buffer(input)
.map_err(|e| reason_err!("CredSSP", "TsRequest decode: {e}"))?;
debug!(message = ?ts_request_from_server, "Received");
let result = credssp_client
.process(ts_request_from_server)
.map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?;
@@ -463,6 +465,8 @@ impl Sequence for ClientConnector {
let early_user_auth_result = credssp::EarlyUserAuthResult::from_buffer(input)
.map_err(|e| custom_err!("credssp::EarlyUserAuthResult", e))?;
debug!(message = ?early_user_auth_result, "Received");
let credssp::EarlyUserAuthResult::Success = early_user_auth_result else {
return Err(ConnectorError::new("CredSSP", ConnectorErrorKind::AccessDenied));
};
+1 -1
View File
@@ -47,7 +47,7 @@ where
Box::pin(async {
// NOTE(perf): tokio implementation is more efficient
let mut read_bytes = [0u8; 1024];
let len = self.inner.read(&mut read_bytes[..]).await?;
let len = self.inner.read(&mut read_bytes).await?;
buf.extend_from_slice(&read_bytes[..len]);
Ok(len)
+2 -2
View File
@@ -65,7 +65,7 @@ pub trait StaticVirtualChannel: AsAny + fmt::Debug + Send + Sync {
/// Returns the name of the `StaticVirtualChannel`
fn channel_name(&self) -> ChannelName;
/// Defines which compression flag should be sent along the [`Channel`] Definition Structure (`CHANNEL_DEF`)
/// Defines which compression flag should be sent along the [`ChannelDef`] Definition Structure (`CHANNEL_DEF`)
fn compression_condition(&self) -> CompressionCondition {
CompressionCondition::Never
}
@@ -83,7 +83,7 @@ pub trait StaticVirtualChannel: AsAny + fmt::Debug + Send + Sync {
assert_obj_safe!(StaticVirtualChannel);
/// Takes a vector of PDUs and breaks them into chunks prefixed with a [`ChannelPduHeader`].
/// Takes a vector of PDUs and breaks them into chunks prefixed with a Channel PDU Header (`CHANNEL_PDU_HEADER`).
///
/// Each chunk is at most `max_chunk_len` bytes long (not including the Channel PDU Header).
pub fn chunkify(messages: Vec<SvcMessage>, max_chunk_len: usize) -> PduResult<Vec<WriteBuf>> {
+7 -4
View File
@@ -14,10 +14,6 @@ where
{
#[cfg(feature = "rustls")]
let mut tls_stream = {
// FIXME: disable TLS session resume just to be safe (not unsupported by CredSSP server)
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c
// Option is available starting rustls 0.21
let mut config = tokio_rustls::rustls::client::ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification))
@@ -26,6 +22,13 @@ where
// This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret)
config.key_log = std::sync::Arc::new(tokio_rustls::rustls::KeyLogFile::new());
// Disable TLS resumption because its not supported by some services such as CredSSP.
//
// > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported.
//
// source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c
config.resumption = tokio_rustls::rustls::client::Resumption::disabled();
let config = std::sync::Arc::new(config);
let server_name = server_name.try_into().unwrap();
-12
View File
@@ -1,17 +1,5 @@
use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _};
use ironrdp::session::image::DecodedImage;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct RectInfo {
pub frame_id: usize,
pub top: u16,
pub left: u16,
pub right: u16,
pub bottom: u16,
pub width: u16,
pub height: u16,
}
pub fn extract_partial_image(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec<u8>) {
// PERF: needs actual benchmark to find a better heuristic
+13 -2
View File
@@ -11,6 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
all-features = true
[lib]
doctest = false
test = false
@@ -44,5 +48,12 @@ ironrdp-dvc = { workspace = true, optional = true }
ironrdp-rdpdr = { workspace = true, optional = true }
ironrdp-rdpsnd = { workspace = true, optional = true }
[package.metadata.docs.rs]
all-features = true
[dev-dependencies]
ironrdp-blocking.workspace = true
anyhow = "1"
rustls = "0.21"
bmp = "0.5"
pico-args = "0.5"
x509-cert = { version = "0.2.1", default-features = false, features = ["std"] }
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Some files were not shown because too many files have changed in this diff Show More