mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat(blocking): initial implementation (#158)
This commit is contained in:
@@ -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
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
mod connector;
|
||||
mod framed;
|
||||
mod session;
|
||||
|
||||
pub use connector::*;
|
||||
pub use framed::*;
|
||||
pub use session::*;
|
||||
@@ -0,0 +1 @@
|
||||
// TODO: active session I/O helpers? I’m not yet sure we need that
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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));
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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 it’s 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user