refactor: clarify project architecture (#123)

> Make the root of the workspace a virtual manifest. It might
> be tempting to put the main crate into the root, but that
> pollutes the root with src/, requires passing --workspace to
> every Cargo command, and adds an exception to an otherwise
> consistent structure.

> Don’t succumb to the temptation to strip common prefix
> from folder names. If each crate is named exactly as the
> folder it lives in, navigation and renames become easier.
> Cargo.tomls of reverse dependencies mention both the folder
> and the crate name, it’s useful when they are exactly the
> same.

Source:
https://matklad.github.io/2021/08/22/large-rust-workspaces.html#Smaller-Tips
This commit is contained in:
Benoît Cortier 2023-05-09 17:00:07 -04:00 committed by GitHub
parent a2c6a5c124
commit 55d11a5000
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
296 changed files with 577 additions and 450 deletions

110
ARCHITECTURE.md Normal file
View file

@ -0,0 +1,110 @@
# Architecture
This document describes the high-level architecture of IronRDP.
> Roughly, it takes 2x more time to write a patch if you are unfamiliar with the
> project, but it takes 10x more time to figure out where you should change the
> code.
[Source](https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html)
## Code Map
This section talks briefly about various important directories and data structures.
### Core Crates
- `crates/ironrdp`: meta crate re-exporting important crates.
- `crates/ironrdp-pdu`: PDU encoding and decoding (no I/O, trivial to fuzz). <!-- TODO: important types and traits (PduDecode, PduEncode…) -->
- `crates/ironrdp-graphics`: image processing primitives (no I/O, trivial to fuzz).
- `crates/ironrdp-connector`: state machines to drive an RDP connection sequence (no I/O, not _too_ hard to fuzz).
- `crates/ironrdp-session`: state machines to drive an RDP session (no I/O, not _too_ hard to fuzz).
- `crates/ironrdp-input`: utilities to manage and build input packets (no I/O).
- `crates/ironrdp-rdcleanpath`: RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway.
### Utility Crates
- `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.
- `crates/ironrdp-tls`: TLS boilerplate common with most IronRDP clients.
### Client Crates
- `crates/ironrdp-client`: portable RDP client without GPU acceleration using softbuffer and winit for windowing.
- `crates/ironrdp-web`: WebAssembly high-level bindings targeting web browsers.
- `crates/ironrdp-glutin-renderer`: `glutin` primitives for OpenGL rendering.
- `crates/ironrdp-client-glutin`: GPU-accelerated RDP client using glutin.
- `crates/ironrdp-replay-client`: utility tool to replay RDP graphics pipeline for debugging purposes.
- `web-client/iron-remote-gui`: core frontend UI used by `iron-svelte-client` as a Web Component.
- `web-client/iron-svelte-client`: web-based frontend using `Svelte` and `Material` frameworks.
### Private Crates
Crates that are only used inside the IronRDP project, not meant to be published.
- `crates/ironrdp-pdu-generators`: `proptest` generators for `ironrdp-pdu` types.
- `crates/ironrdp-session-generators`: `proptest` generators for `ironrdp-session` types.
- `fuzz`: fuzz targets for core crates.
- `xtask`: IronRDPs free-form automation using Rust code.
## Cross-Cutting Concerns
This section talks about the things which are everywhere and nowhere in particular.
### General
- Dependency injection when runtime information is necessary in core crates (no system call such as `gethostname`)
- Keep non-portable code out of core crates
- Make crate `no_std`-compatible wherever possible
- Facilitate fuzzing
- In libraries, provide concrete error types either hand-crafted or using `thiserror` crate
- In binaries, use the convenient catch-all error type `anyhow::Error`
- Free-form automation a-la `make` following [`cargo xtask`](https://github.com/matklad/cargo-xtask) specification
### Avoid I/O wherever possible
**Architecture Invariant**: core crates must never interact with the outside world. Only client and utility crates
such as `ironrdp-client`, `ironrdp-web` or `ironrdp-async` are allowed to do I/O.
### Continuous integration
We use GitHub action and our workflows simply run `cargo xtask`.
The expectation is that, if `cargo xtask ci` passes locally, the CI will be green as well.
**Architecture Invariant**: `cargo xtask ci` and CI workflow must be logically equivalents. It must
be the case that a successful `cargo xtask ci` run implies a successful CI workflow run and vice versa.
### Testing
#### Fuzzing
See [`fuzz/README.md`](../fuzz/README.md).
#### Readability
Do not include huge binary chunks directly in source files (`*.rs`). Place these in separate files (`*.bin`, `*.bmp`)
and include them using macros such as `include_bytes!` or `include_str!`.
#### Use `expect-test` for snapshot testing
When comparing structured data (e.g.: error results, decoded PDUs), use `expect-test`. It is both easy to create
and maintain such tests. When something affecting the representation is changed, simply run the test again with
`UPDATE_EXPECT=1` env variable to magically update the code.
See:
- <https://matklad.github.io/2021/05/31/how-to-test.html#Expect-Tests>
- <https://docs.rs/expect-test/latest/expect_test/>
TODO: take further inspiration from rust-analyzer
- https://github.com/rust-lang/rust-analyzer/blob/d7c99931d05e3723d878bea5dc26766791fa4e69/docs/dev/architecture.md#testing
- https://matklad.github.io/2021/05/31/how-to-test.html
#### Use `rstest` for fixture-based testing
When a test can be generalized for multiple inputs, use [`rstest`](https://github.com/la10736/rstest) to avoid code duplication.
#### Use `proptest` for property testing
It allows to test that certain properties of your code hold for arbitrary inputs, and if a failure
is found, automatically finds the minimal test case to reproduce the problem.

29
Cargo.lock generated
View file

@ -1509,12 +1509,9 @@ name = "ironrdp-async"
version = "0.1.0"
dependencies = [
"bytes",
"futures-util",
"ironrdp-connector",
"ironrdp-pdu",
"tap",
"tokio",
"tokio-util",
"tracing",
]
@ -1526,18 +1523,16 @@ dependencies = [
"chrono",
"clap",
"exitcode",
"futures-util",
"inquire",
"ironrdp",
"ironrdp-async",
"ironrdp-tls",
"ironrdp-tokio",
"semver",
"smallvec",
"softbuffer",
"sspi",
"tap",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"whoami",
@ -1556,6 +1551,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "ironrdp-futures"
version = "0.1.0"
dependencies = [
"bytes",
"futures-util",
"ironrdp-async",
]
[[package]]
name = "ironrdp-graphics"
version = "0.1.0"
@ -1669,6 +1673,16 @@ dependencies = [
"x509-cert",
]
[[package]]
name = "ironrdp-tokio"
version = "0.1.0"
dependencies = [
"bytes",
"ironrdp-async",
"tokio",
"tokio-util",
]
[[package]]
name = "ironrdp-web"
version = "0.1.0"
@ -1681,7 +1695,7 @@ dependencies = [
"getrandom 0.2.8",
"gloo-net",
"ironrdp",
"ironrdp-async",
"ironrdp-futures",
"ironrdp-rdcleanpath",
"js-sys",
"semver",
@ -3798,7 +3812,6 @@ checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",

View file

@ -1,52 +1,14 @@
[package]
name = "ironrdp"
version = "0.5.0"
readme = "README.md"
description = "A Rust implementation of the Microsoft Remote Desktop Protocol (RDP)"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[features]
default = ["pdu", "connector", "session"]
pdu = ["dep:ironrdp-pdu"]
connector = ["dep:ironrdp-connector"]
session = ["dep:ironrdp-session"]
graphics = ["dep:ironrdp-graphics"]
input = ["dep:ironrdp-input"]
[dependencies]
ironrdp-pdu = { workspace = true, optional = true }
ironrdp-connector = { workspace = true, optional = true }
ironrdp-session = { workspace = true, optional = true }
ironrdp-graphics = { workspace = true, optional = true }
ironrdp-input = { workspace = true, optional = true }
[workspace]
members = [
"crates/*",
"xtask",
]
default-members = [
"crates/pdu",
"crates/connector",
"crates/session",
"crates/graphics",
"crates/input",
"crates/async",
"crates/client",
"crates/web",
]
# FIXME: fix compilation
exclude = [
"crates/client-glutin",
"crates/glutin-renderer",
"crates/replay-client",
"crates/ironrdp-client-glutin",
"crates/ironrdp-glutin-renderer",
"crates/ironrdp-replay-client",
]
[workspace.package]
@ -60,18 +22,20 @@ categories = ["network-programming"]
[workspace.dependencies]
expect-test = "1"
ironrdp = { version = "0.5", path = "." }
ironrdp-async = { version = "0.1", path = "crates/async" }
ironrdp-connector = { version = "0.1", path = "crates/connector" }
ironrdp-graphics = { version = "0.1", path = "crates/graphics" }
ironrdp-input = { version = "0.1", path = "crates/input" }
ironrdp-pdu = { version = "0.1", path = "crates/pdu" }
ironrdp-pdu-generators = { path = "crates/pdu-generators" }
ironrdp-pdu-samples = { path = "crates/pdu-samples" }
ironrdp-rdcleanpath = { version = "0.1", path = "crates/rdcleanpath" }
ironrdp-session = { version = "0.1", path = "crates/session" }
ironrdp-session-generators = { path = "crates/session-generators" }
ironrdp-tls = { version = "0.1", path = "crates/tls" }
ironrdp-async = { version = "0.1", path = "crates/ironrdp-async" }
ironrdp-connector = { version = "0.1", path = "crates/ironrdp-connector" }
ironrdp-futures = { version = "0.1", path = "crates/ironrdp-futures" }
ironrdp-graphics = { version = "0.1", path = "crates/ironrdp-graphics" }
ironrdp-input = { version = "0.1", path = "crates/ironrdp-input" }
ironrdp-pdu-generators = { path = "crates/ironrdp-pdu-generators" }
ironrdp-pdu-samples = { path = "crates/ironrdp-pdu-samples" }
ironrdp-pdu = { version = "0.1", path = "crates/ironrdp-pdu" }
ironrdp-rdcleanpath = { version = "0.1", path = "crates/ironrdp-rdcleanpath" }
ironrdp-session-generators = { path = "crates/ironrdp-session-generators" }
ironrdp-session = { version = "0.1", path = "crates/ironrdp-session" }
ironrdp-tls = { version = "0.1", path = "crates/ironrdp-tls" }
ironrdp-tokio = { version = "0.1", path = "crates/ironrdp-tokio" }
ironrdp = { version = "0.5", path = "crates/ironrdp" }
proptest = "1.1.0"
rstest = "0.17.0"
sspi = "0.8.1"

View file

@ -35,40 +35,6 @@ Alternatively, you may change a few group policies using `gpedit.msc`:
5. Reboot.
## Architecture (Work In Progress…)
## Architecture
- `ironrdp` (root package): meta crate re-exporting important crates,
- `ironrdp-pdu` (`crates/pdu`): PDU encoding and decoding (no I/O, trivial to fuzz),
- `ironrdp-graphics` (`crates/graphics`): image processing primitives (no I/O, trivial to fuzz),
- `ironrdp-connector` (`crates/connector`): state machines to drive an RDP connection sequence (no I/O, not _too_ hard to fuzz),
- `ironrdp-session` (`crates/session`): state machines to drive an RDP session (no I/O, not _too_ hard to fuzz),
- `ironrdp-input` (`crates/input`): utilities to manage and build input packets (no I/O),
- `ironrdp-async` (`crates/async`): provides `Future`s wrapping the state machines conveniently,
- `ironrdp-tls` (`crates/tls`): TLS boilerplate common with most IronRDP clients,
- `ironrdp-rdcleanpath` (`crates/rdcleanpath`): RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway,
- `ironrdp-client` (`crates/client`): Portable RDP client without GPU acceleration using softbuffer and winit for windowing,
- `ironrdp-web` (`crates/web`): WebAssembly high-level bindings targeting web browsers,
- `ironrdp-glutin-renderer` (`crates/glutin-renderer`): `glutin` primitives for OpenGL rendering,
- `ironrdp-client-glutin` (`crates/client-glutin`): GPU-accelerated RDP client using glutin,
- `ironrdp-replay-client` (`crates/replay-client`): utility tool to replay RDP graphics pipeline for debugging purposes,
- `ironrdp-pdu-generators` (`crates/pdu-generators`): `proptest` generators for `ironrdp-pdu` types,
- `ironrdp-session-generators` (`crates/session-generators`): `proptest` generators for `ironrdp-session` types,
- `iron-remote-gui` (`web-client/iron-remote-gui`): core frontend UI used by `iron-svelte-client` as a Web Component,
- `iron-svelte-client` (`web-client/iron-svelte-client`): web-based frontend using `Svelte` and `Material` frameworks,
- and finally, `ironrdp-fuzz` (`fuzz`): fuzz targets for core crates.
## General design
- Avoid I/O wherever possible
- Dependency injection when runtime information is necessary in core crates (no system call such as `gethostname`)
- Keep non-portable code out of core crates
- Make crate `no_std`-compatible wherever possible
- Facilitate fuzzing
- In libraries, provide concrete error types either hand-crafted or using `thiserror` crate
- In binaries, use the convenient catch-all error type `anyhow::Error`
- Free-form automation a-la `make` following [`cargo xtask`](https://github.com/matklad/cargo-xtask) specification
## Continuous integration
We use GitHub action and our workflows simply run `cargo xtask`.
The expectation is that, if `cargo xtask ci` passes locally, the CI will be green as well.
See the [ARCHITECTURE.md](./ARCHITECTURE.md) document.

View file

@ -1,307 +0,0 @@
use std::io;
use std::pin::Pin;
use bytes::{Bytes, BytesMut};
use ironrdp_pdu::PduHint;
// TODO: use static async fn / return position impl trait in traits where stabiziled (https://github.com/rust-lang/rust/issues/91611)
pub trait FramedRead: private::Sealed {
/// Reads from stream and fills internal buffer
fn read<'a>(
&'a mut self,
buf: &'a mut BytesMut,
) -> Pin<Box<dyn std::future::Future<Output = io::Result<usize>> + 'a>>
where
Self: 'a;
}
pub trait FramedWrite: private::Sealed {
/// Writes an entire buffer into this stream.
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn std::future::Future<Output = io::Result<()>> + 'a>>
where
Self: 'a;
}
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
}
}
#[cfg(feature = "tokio")]
impl<S> Framed<TokioCompat<S>> {
pub fn tokio_new(stream: S) -> Self {
Self {
stream: TokioCompat { inner: stream },
buf: BytesMut::new(),
}
}
pub fn tokio_into_inner(self) -> (S, BytesMut) {
(self.stream.inner, self.buf)
}
pub fn tokio_into_inner_no_leftover(self) -> S {
let (stream, leftover) = self.tokio_into_inner();
assert_eq!(leftover.len(), 0, "unexpected leftover");
stream
}
pub fn tokio_get_inner(&self) -> (&S, &BytesMut) {
(&self.stream.inner, &self.buf)
}
pub fn tokio_get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) {
(&mut self.stream.inner, &mut self.buf)
}
}
#[cfg(feature = "futures")]
impl<S> Framed<FuturesCompat<S>> {
pub fn futures_new(stream: S) -> Self {
Self {
stream: FuturesCompat { inner: stream },
buf: BytesMut::new(),
}
}
pub fn futures_into_inner(self) -> (S, BytesMut) {
(self.stream.inner, self.buf)
}
pub fn futures_into_inner_no_leftover(self) -> S {
let (stream, leftover) = self.futures_into_inner();
debug_assert_eq!(leftover.len(), 0, "unexpected leftover");
stream
}
pub fn futures_get_inner(&self) -> (&S, &BytesMut) {
(&self.stream.inner, &self.buf)
}
pub fn futures_get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) {
(&mut self.stream.inner, &mut self.buf)
}
}
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
}
pub async fn read_exact(&mut self, length: usize) -> io::Result<Bytes> {
loop {
if self.buf.len() >= length {
return Ok(self.buf.split_to(length).freeze());
} else {
self.buf.reserve(length - self.buf.len());
}
let len = self.read().await?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
}
pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, Bytes)> {
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).await?;
return Ok((pdu_info.action, frame));
}
Ok(None) => {
let len = self.read().await?;
// 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)),
};
}
}
pub async 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 self.read_exact(length).await;
}
None => {
let len = self.read().await?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
};
}
}
}
impl<S> Framed<S>
where
S: FramedWrite,
{
/// Reads from stream and fills internal buffer
pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.stream.write_all(buf).await
}
}
#[cfg(feature = "tokio")]
pub struct TokioCompat<S> {
inner: S,
}
#[cfg(feature = "tokio")]
mod tokio_impl {
use tokio::io::{AsyncRead, AsyncWrite};
use super::*;
impl<S> private::Sealed for TokioCompat<S> {}
impl<S> FramedRead for TokioCompat<S>
where
S: Unpin + AsyncRead,
{
fn read<'a>(
&'a mut self,
buf: &'a mut BytesMut,
) -> Pin<Box<dyn std::future::Future<Output = io::Result<usize>> + 'a>>
where
Self: 'a,
{
use tokio::io::AsyncReadExt as _;
Box::pin(async { self.inner.read_buf(buf).await })
}
}
impl<S> FramedWrite for TokioCompat<S>
where
S: Unpin + AsyncWrite,
{
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn std::future::Future<Output = io::Result<()>> + 'a>>
where
Self: 'a,
{
use tokio::io::AsyncWriteExt as _;
Box::pin(async {
self.inner.write_all(buf).await?;
self.inner.flush().await?;
Ok(())
})
}
}
}
#[cfg(feature = "futures")]
pub struct FuturesCompat<S> {
pub(super) inner: S,
}
#[cfg(feature = "futures")]
mod futures_impl {
pub use futures_util::io::{AsyncRead, AsyncWrite};
use super::*;
impl<S> private::Sealed for FuturesCompat<S> {}
impl<S> FramedRead for FuturesCompat<S>
where
S: Unpin + AsyncRead,
{
fn read<'a>(
&'a mut self,
buf: &'a mut BytesMut,
) -> Pin<Box<dyn std::future::Future<Output = io::Result<usize>> + 'a>>
where
Self: 'a,
{
use futures_util::io::AsyncReadExt as _;
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?;
buf.extend_from_slice(&read_bytes[..len]);
Ok(len)
})
}
}
impl<S> FramedWrite for FuturesCompat<S>
where
S: Unpin + AsyncWrite,
{
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn std::future::Future<Output = io::Result<()>> + 'a>>
where
Self: 'a,
{
use futures_util::io::AsyncWriteExt as _;
Box::pin(async {
self.inner.write_all(buf).await?;
self.inner.flush().await?;
Ok(())
})
}
}
}
mod private {
pub trait Sealed {}
}

View file

@ -11,25 +11,10 @@ authors.workspace = true
keywords.workspace = true
categories.workspace = true
[features]
default = ["tokio"]
tokio = ["dep:tokio", "dep:tokio-util"]
futures = ["dep:futures-util"]
[dependencies]
# Protocols
ironrdp-pdu.workspace = true
bytes = "1"
ironrdp-connector.workspace = true
ironrdp-pdu.workspace = true
# ironrdp-session.workspace = true
# Logging
tap = "1"
tracing.workspace = true
# Utils
bytes = "1.4.0"
tap = "1.0.1"
# Async
tokio = { version = "1.25.0", features = ["io-util"], optional = true }
tokio-util = { version = "0.7.7", features = ["codec"], optional = true }
futures-util = { version = "0.3.26", features = ["io"], optional = true }

View file

@ -0,0 +1,157 @@
use std::io;
use std::pin::Pin;
use bytes::{Bytes, BytesMut};
use ironrdp_pdu::PduHint;
// TODO: use static async fn / return position impl trait in traits when stabiziled (https://github.com/rust-lang/rust/issues/91611)
pub trait FramedRead {
/// Reads from stream and fills internal buffer
fn read<'a>(
&'a mut self,
buf: &'a mut BytesMut,
) -> Pin<Box<dyn std::future::Future<Output = io::Result<usize>> + 'a>>
where
Self: 'a;
}
pub trait FramedWrite {
/// Writes an entire buffer into this stream.
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn std::future::Future<Output = io::Result<()>> + 'a>>
where
Self: 'a;
}
pub trait StreamWrapper: Sized {
type InnerStream;
fn from_inner(stream: Self::InnerStream) -> Self;
fn into_inner(self) -> Self::InnerStream;
fn get_inner(&self) -> &Self::InnerStream;
fn get_inner_mut(&mut self) -> &mut Self::InnerStream;
}
pub struct Framed<S> {
stream: S,
buf: BytesMut,
}
impl<S> Framed<S> {
pub fn peek(&self) -> &[u8] {
&self.buf
}
}
impl<S> Framed<S>
where
S: StreamWrapper,
{
pub fn new(stream: S::InnerStream) -> Self {
Self {
stream: S::from_inner(stream),
buf: BytesMut::new(),
}
}
pub fn into_inner(self) -> (S::InnerStream, BytesMut) {
(self.stream.into_inner(), self.buf)
}
pub fn into_inner_no_leftover(self) -> S::InnerStream {
let (stream, leftover) = self.into_inner();
debug_assert_eq!(leftover.len(), 0, "unexpected leftover");
stream
}
pub fn get_inner(&self) -> (&S::InnerStream, &BytesMut) {
(self.stream.get_inner(), &self.buf)
}
pub fn get_inner_mut(&mut self) -> (&mut S::InnerStream, &mut BytesMut) {
(self.stream.get_inner_mut(), &mut self.buf)
}
}
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
}
pub async fn read_exact(&mut self, length: usize) -> io::Result<Bytes> {
loop {
if self.buf.len() >= length {
return Ok(self.buf.split_to(length).freeze());
} else {
self.buf.reserve(length - self.buf.len());
}
let len = self.read().await?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
}
pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, Bytes)> {
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).await?;
return Ok((pdu_info.action, frame));
}
Ok(None) => {
let len = self.read().await?;
// 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)),
};
}
}
pub async 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 self.read_exact(length).await;
}
None => {
let len = self.read().await?;
// Handle EOF
if len == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes"));
}
}
};
}
}
}
impl<S> Framed<S>
where
S: FramedWrite,
{
/// Reads from stream and fills internal buffer
pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.stream.write_all(buf).await
}
}

View file

@ -21,7 +21,7 @@ native-tls = ["ironrdp-tls/native-tls"]
# Protocols
ironrdp = { workspace = true, features = ["input", "graphics"] }
ironrdp-tls.workspace = true
ironrdp-async = { workspace = true, features = ["tokio"] }
ironrdp-tokio.workspace = true
sspi = { workspace = true, features = ["network_client"] } # TODO: enable dns_resolver at some point
# GUI
@ -39,8 +39,6 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
# Async, futures
tokio = { version = "1", features = ["full"]}
tokio-util = { version = "0.7.7", features = ["compat"] }
futures-util = "0.3"
# Utils
chrono = "0.4.24"

View file

@ -78,7 +78,7 @@ enum RdpControlFlow {
TerminatedGracefully,
}
type UpgradedFramed = ironrdp_async::Framed<ironrdp_async::TokioCompat<ironrdp_tls::TlsStream<TcpStream>>>;
type UpgradedFramed = ironrdp_tokio::TokioFramed<ironrdp_tls::TlsStream<TcpStream>>;
async fn connect(config: &Config) -> connector::Result<(connector::ConnectionResult, UpgradedFramed)> {
let server_addr = config
@ -90,29 +90,29 @@ async fn connect(config: &Config) -> connector::Result<(connector::ConnectionRes
.await
.map_err(|e| connector::Error::new("TCP connect").with_custom(e))?;
let mut framed = ironrdp_async::Framed::tokio_new(stream);
let mut framed = ironrdp_tokio::TokioFramed::new(stream);
let mut connector = connector::ClientConnector::new(config.connector.clone())
.with_server_addr(server_addr)
.with_server_name(&config.destination)
.with_credssp_client_factory(Box::new(RequestClientFactory));
let should_upgrade = ironrdp_async::connect_begin(&mut framed, &mut connector).await?;
let should_upgrade = ironrdp_tokio::connect_begin(&mut framed, &mut connector).await?;
debug!("TLS upgrade");
// Ensure there is no leftover
let initial_stream = framed.tokio_into_inner_no_leftover();
let initial_stream = framed.into_inner_no_leftover();
let (upgraded_stream, server_public_key) = ironrdp_tls::upgrade(initial_stream, config.destination.name())
.await
.map_err(|e| connector::Error::new("TLS upgrade").with_custom(e))?;
let upgraded = ironrdp_async::mark_as_upgraded(should_upgrade, &mut connector, server_public_key);
let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector, server_public_key);
let mut upgraded_framed = ironrdp_async::Framed::tokio_new(upgraded_stream);
let mut upgraded_framed = ironrdp_tokio::TokioFramed::new(upgraded_stream);
let connection_result = ironrdp_async::connect_finalize(upgraded, &mut upgraded_framed, connector).await?;
let connection_result = ironrdp_tokio::connect_finalize(upgraded, &mut upgraded_framed, connector).await?;
Ok((connection_result, upgraded_framed))
}

View file

@ -0,0 +1,17 @@
[package]
name = "ironrdp-futures"
version = "0.1.0"
readme = "README.md"
description = "`Framed*` traits implementation above futuress traits"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[dependencies]
bytes = "1"
futures-util = { version = "0.3.26", features = ["io"] }
ironrdp-async.workspace = true

View file

@ -0,0 +1,76 @@
use std::io;
use std::pin::Pin;
use bytes::BytesMut;
use futures_util::io::{AsyncRead, AsyncWrite};
pub use ironrdp_async::*;
pub type FuturesFramed<S> = Framed<FuturesStream<S>>;
pub struct FuturesStream<S> {
inner: S,
}
impl<S> StreamWrapper for FuturesStream<S> {
type InnerStream = S;
fn from_inner(stream: Self::InnerStream) -> Self {
Self { inner: stream }
}
fn into_inner(self) -> Self::InnerStream {
self.inner
}
fn get_inner(&self) -> &Self::InnerStream {
&self.inner
}
fn get_inner_mut(&mut self) -> &mut Self::InnerStream {
&mut self.inner
}
}
impl<S> FramedRead for FuturesStream<S>
where
S: Unpin + AsyncRead,
{
fn read<'a>(
&'a mut self,
buf: &'a mut BytesMut,
) -> Pin<Box<dyn std::future::Future<Output = io::Result<usize>> + 'a>>
where
Self: 'a,
{
use futures_util::io::AsyncReadExt as _;
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?;
buf.extend_from_slice(&read_bytes[..len]);
Ok(len)
})
}
}
impl<S> FramedWrite for FuturesStream<S>
where
S: Unpin + AsyncWrite,
{
fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Pin<Box<dyn std::future::Future<Output = io::Result<()>> + 'a>>
where
Self: 'a,
{
use futures_util::io::AsyncWriteExt as _;
Box::pin(async {
self.inner.write_all(buf).await?;
self.inner.flush().await?;
Ok(())
})
}
}

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

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