Skip to content

Commit

Permalink
feat(blocking): initial implementation (#158)
Browse files Browse the repository at this point in the history
  • Loading branch information
CBenoit authored Sep 11, 2023
1 parent 9d33cad commit babbd68
Show file tree
Hide file tree
Showing 21 changed files with 826 additions and 79 deletions.
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 20 additions & 1 deletion Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 58 additions & 6 deletions crates/ironrdp-async/src/framed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}

Expand All @@ -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>;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand Down
24 changes: 24 additions & 0 deletions crates/ironrdp-blocking/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions crates/ironrdp-blocking/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 116 additions & 0 deletions crates/ironrdp-blocking/src/connector.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit babbd68

Please sign in to comment.