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
110
ARCHITECTURE.md
Normal 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`: IronRDP’s 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
|
@ -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",
|
||||
|
|
70
Cargo.toml
|
@ -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"
|
||||
|
|
38
README.md
|
@ -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.
|
||||
|
|
|
@ -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 {}
|
||||
}
|
|
@ -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 }
|
157
crates/ironrdp-async/src/framed.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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))
|
||||
}
|
17
crates/ironrdp-futures/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "ironrdp-futures"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
description = "`Framed*` traits implementation above futures’s 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
|
76
crates/ironrdp-futures/src/lib.rs
Normal 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(())
|
||||
})
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |