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

View file

@ -0,0 +1,20 @@
[package]
name = "ironrdp-rdcleanpath"
version = "0.1.0"
readme = "README.md"
description = "RDCleanPath PDU structure used by IronRDP web client and Devolutions Gateway"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[dependencies]
der = { version = "0.7.1", features = ["alloc", "derive"] }
[dev-dependencies]
rstest = "0.17.0"
hex = "0.4.3"
pretty_assertions = "1.3.0"

View file

@ -0,0 +1,3 @@
# IronRDP RDCleanPath
RDCleanPath PDU structure used by IronRDP and Devolutions Gateway.

View file

@ -0,0 +1,486 @@
use core::fmt;
use der::asn1::OctetString;
pub const BASE_VERSION: u64 = 3389;
pub const VERSION_1: u64 = BASE_VERSION + 1;
pub const GENERAL_ERROR_CODE: u16 = 1;
// Re-export der crate for convenience
pub use der;
#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)]
#[asn1(tag_mode = "EXPLICIT")]
pub struct RDCleanPathErr {
#[asn1(context_specific = "0")]
pub error_code: u16,
#[asn1(context_specific = "1", optional = "true")]
pub http_status_code: Option<u16>,
#[asn1(context_specific = "2", optional = "true")]
pub wsa_last_error: Option<u16>,
#[asn1(context_specific = "3", optional = "true")]
pub tls_alert_code: Option<u8>,
}
impl fmt::Display for RDCleanPathErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RDCleanPath error (code {})", self.error_code)?;
if let Some(http_status_code) = self.http_status_code {
write!(f, " [HTTP status = {http_status_code}]")?;
}
if let Some(wsa_last_error) = self.wsa_last_error {
write!(f, " [WSA last error = {wsa_last_error}]")?;
}
if let Some(tls_alert_code) = self.tls_alert_code {
write!(f, " [TLS alert = {tls_alert_code}]")?;
}
Ok(())
}
}
impl std::error::Error for RDCleanPathErr {}
#[derive(Clone, Debug, Eq, PartialEq, der::Sequence)]
#[asn1(tag_mode = "EXPLICIT")]
pub struct RDCleanPathPdu {
/// RDCleanPathPdu packet version.
#[asn1(context_specific = "0")]
pub version: u64,
/// The proxy error.
///
/// Sent from proxy to client only.
#[asn1(context_specific = "1", optional = "true")]
pub error: Option<RDCleanPathErr>,
/// The RDP server address itself.
///
/// Sent from client to proxy only.
#[asn1(context_specific = "2", optional = "true")]
pub destination: Option<String>,
/// Arbitrary string for authorization on proxy side.
///
/// Sent from client to proxy only.
#[asn1(context_specific = "3", optional = "true")]
pub proxy_auth: Option<String>,
/// Currently unused. Could be used by a custom RDP server eventually.
#[asn1(context_specific = "4", optional = "true")]
pub server_auth: Option<String>,
/// The RDP PCB forwarded by the proxy to the RDP server.
///
/// Sent from client to proxy only.
#[asn1(context_specific = "5", optional = "true")]
pub preconnection_blob: Option<String>,
/// Either the client handshake or the server handshake response.
///
/// Both client and proxy will set this field.
#[asn1(context_specific = "6", optional = "true")]
pub x224_connection_pdu: Option<OctetString>,
/// The RDP server TLS chain.
///
/// Sent from proxy to client only.
#[asn1(context_specific = "7", optional = "true")]
pub server_cert_chain: Option<Vec<OctetString>>,
// #[asn1(context_specific = "8", optional = "true")]
// pub ocsp_response: Option<String>,
/// IPv4 or IPv6 address of the server found by resolving the destination field on proxy side.
///
/// Sent from proxy to client only.
#[asn1(context_specific = "9", optional = "true")]
pub server_addr: Option<String>,
}
impl Default for RDCleanPathPdu {
fn default() -> Self {
Self {
version: VERSION_1,
error: None,
destination: None,
proxy_auth: None,
server_auth: None,
preconnection_blob: None,
x224_connection_pdu: None,
server_cert_chain: None,
server_addr: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DetectionResult {
Detected { version: u64, total_length: usize },
NotEnoughBytes,
Failed,
}
impl RDCleanPathPdu {
/// Attempts to decode a RDCleanPath PDU from the provided buffer of bytes.
pub fn from_der(src: &[u8]) -> der::Result<Self> {
der::Decode::from_der(src)
}
/// Try to parse first few bytes in order to detect a RDCleanPath PDU
pub fn detect(src: &[u8]) -> DetectionResult {
use der::{Decode as _, Encode as _};
let Ok(mut reader) = der::SliceReader::new(src) else {
return DetectionResult::Failed
};
let header = match der::Header::decode(&mut reader) {
Ok(header) => header,
Err(e) => match e.kind() {
der::ErrorKind::Incomplete { .. } => return DetectionResult::NotEnoughBytes,
_ => return DetectionResult::Failed,
},
};
let (Ok(header_encoded_len), Ok(body_length)) = (header.encoded_len().and_then(usize::try_from), usize::try_from(header.length)) else {
return DetectionResult::Failed;
};
let total_length = header_encoded_len + body_length;
match der::asn1::ContextSpecific::<u64>::decode_explicit(&mut reader, der::TagNumber::N0) {
Ok(Some(version)) if version.value == VERSION_1 => DetectionResult::Detected {
version: VERSION_1,
total_length,
},
Ok(Some(_)) => DetectionResult::Failed,
Ok(None) => DetectionResult::NotEnoughBytes,
Err(e) => match e.kind() {
der::ErrorKind::Incomplete { .. } => DetectionResult::NotEnoughBytes,
_ => DetectionResult::Failed,
},
}
}
pub fn into_enum(self) -> Result<RDCleanPath, MissingRDCleanPathField> {
RDCleanPath::try_from(self)
}
pub fn new_general_error() -> Self {
Self {
version: VERSION_1,
error: Some(RDCleanPathErr {
error_code: GENERAL_ERROR_CODE,
http_status_code: None,
wsa_last_error: None,
tls_alert_code: None,
}),
..Self::default()
}
}
pub fn new_http_error(status_code: u16) -> Self {
Self {
version: VERSION_1,
error: Some(RDCleanPathErr {
error_code: GENERAL_ERROR_CODE,
http_status_code: Some(status_code),
wsa_last_error: None,
tls_alert_code: None,
}),
..Self::default()
}
}
pub fn new_request(
x224_pdu: Vec<u8>,
destination: String,
proxy_auth: String,
pcb: Option<String>,
) -> der::Result<Self> {
Ok(Self {
version: VERSION_1,
destination: Some(destination),
proxy_auth: Some(proxy_auth),
preconnection_blob: pcb,
x224_connection_pdu: Some(OctetString::new(x224_pdu)?),
..Self::default()
})
}
pub fn new_response(
server_addr: String,
x224_pdu: Vec<u8>,
x509_chain: impl IntoIterator<Item = Vec<u8>>,
) -> der::Result<Self> {
Ok(Self {
version: VERSION_1,
x224_connection_pdu: Some(OctetString::new(x224_pdu)?),
server_cert_chain: Some(
x509_chain
.into_iter()
.map(OctetString::new)
.collect::<der::Result<_>>()?,
),
server_addr: Some(server_addr),
..Self::default()
})
}
pub fn new_tls_error(alert_code: u8) -> Self {
Self {
version: VERSION_1,
error: Some(RDCleanPathErr {
error_code: GENERAL_ERROR_CODE,
http_status_code: None,
wsa_last_error: None,
tls_alert_code: Some(alert_code),
}),
..Self::default()
}
}
pub fn new_wsa_error(wsa_error_code: u16) -> Self {
Self {
version: VERSION_1,
error: Some(RDCleanPathErr {
error_code: GENERAL_ERROR_CODE,
http_status_code: None,
wsa_last_error: Some(wsa_error_code),
tls_alert_code: None,
}),
..Self::default()
}
}
pub fn to_der(&self) -> der::Result<Vec<u8>> {
der::Encode::to_der(self)
}
}
/// Helper enum to leverage Rust pattern matching feature.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RDCleanPath {
Request {
destination: String,
proxy_auth: String,
server_auth: Option<String>,
preconnection_blob: Option<String>,
x224_connection_request: OctetString,
},
Response {
x224_connection_response: OctetString,
server_cert_chain: Vec<OctetString>,
server_addr: String,
},
Err(RDCleanPathErr),
}
impl RDCleanPath {
pub fn into_pdu(self) -> RDCleanPathPdu {
RDCleanPathPdu::from(self)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MissingRDCleanPathField(&'static str);
impl fmt::Display for MissingRDCleanPathField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RDCleanPath is missing {} field", self.0)
}
}
impl std::error::Error for MissingRDCleanPathField {}
impl TryFrom<RDCleanPathPdu> for RDCleanPath {
type Error = MissingRDCleanPathField;
fn try_from(pdu: RDCleanPathPdu) -> Result<Self, Self::Error> {
let rdcleanpath = if let Some(destination) = pdu.destination {
Self::Request {
destination,
proxy_auth: pdu.proxy_auth.ok_or(MissingRDCleanPathField("proxy_auth"))?,
server_auth: pdu.server_auth,
preconnection_blob: pdu.preconnection_blob,
x224_connection_request: pdu
.x224_connection_pdu
.ok_or(MissingRDCleanPathField("x224_connection_pdu"))?,
}
} else if let Some(server_addr) = pdu.server_addr {
Self::Response {
x224_connection_response: pdu
.x224_connection_pdu
.ok_or(MissingRDCleanPathField("x224_connection_pdu"))?,
server_cert_chain: pdu
.server_cert_chain
.ok_or(MissingRDCleanPathField("server_cert_chain"))?,
server_addr,
}
} else {
Self::Err(pdu.error.ok_or(MissingRDCleanPathField("error"))?)
};
Ok(rdcleanpath)
}
}
impl From<RDCleanPath> for RDCleanPathPdu {
fn from(value: RDCleanPath) -> Self {
match value {
RDCleanPath::Request {
destination,
proxy_auth,
server_auth,
preconnection_blob,
x224_connection_request,
} => Self {
version: VERSION_1,
destination: Some(destination),
proxy_auth: Some(proxy_auth),
server_auth,
preconnection_blob,
x224_connection_pdu: Some(x224_connection_request),
..Default::default()
},
RDCleanPath::Response {
x224_connection_response,
server_cert_chain,
server_addr,
} => Self {
version: VERSION_1,
x224_connection_pdu: Some(x224_connection_response),
server_cert_chain: Some(server_cert_chain),
server_addr: Some(server_addr),
..Default::default()
},
RDCleanPath::Err(error) => Self {
version: VERSION_1,
error: Some(error),
..Default::default()
},
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn request() -> RDCleanPathPdu {
RDCleanPathPdu::new_request(
vec![0xDE, 0xAD, 0xBE, 0xFF],
"destination".to_owned(),
"proxy auth".to_owned(),
Some("PCB".to_owned()),
)
.unwrap()
}
const REQUEST_DER: &[u8] = &[
0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA2, 0xD, 0xC, 0xB, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6E, 0x61,
0x74, 0x69, 0x6F, 0x6E, 0xA3, 0xC, 0xC, 0xA, 0x70, 0x72, 0x6F, 0x78, 0x79, 0x20, 0x61, 0x75, 0x74, 0x68, 0xA5,
0x5, 0xC, 0x3, 0x50, 0x43, 0x42, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF,
];
fn response_success() -> RDCleanPathPdu {
RDCleanPathPdu::new_response(
"192.168.7.95".to_owned(),
vec![0xDE, 0xAD, 0xBE, 0xFF],
[
vec![0xDE, 0xAD, 0xBE, 0xFF],
vec![0xDE, 0xAD, 0xBE, 0xFF],
vec![0xDE, 0xAD, 0xBE, 0xFF],
],
)
.unwrap()
}
const RESPONSE_SUCCESS_DER: &[u8] = &[
0x30, 0x34, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA6, 0x6, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0xA7, 0x14, 0x30,
0x12, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF, 0x4, 0x4, 0xDE, 0xAD, 0xBE, 0xFF,
0xA9, 0xE, 0xC, 0xC, 0x31, 0x39, 0x32, 0x2E, 0x31, 0x36, 0x38, 0x2E, 0x37, 0x2E, 0x39, 0x35,
];
fn response_http_error() -> RDCleanPathPdu {
RDCleanPathPdu::new_http_error(500)
}
const RESPONSE_HTTP_ERROR_DER: &[u8] = &[
0x30, 0x15, 0xA0, 0x4, 0x2, 0x2, 0xD, 0x3E, 0xA1, 0xD, 0x30, 0xB, 0xA0, 0x3, 0x2, 0x1, 0x1, 0xA1, 0x4, 0x2,
0x2, 0x1, 0xF4,
];
fn response_tls_error() -> RDCleanPathPdu {
RDCleanPathPdu::new_tls_error(48)
}
const RESPONSE_TLS_ERROR_DER: &[u8] = &[
0x30, 0x14, 0xA0, 0x04, 0x02, 0x02, 0x0D, 0x3E, 0xA1, 0x0C, 0x30, 0x0A, 0xA0, 0x03, 0x02, 0x01, 0x01, 0xA3,
0x03, 0x02, 0x01, 0x30,
];
#[rstest]
#[case(request())]
#[case(response_success())]
#[case(response_http_error())]
#[case(response_tls_error())]
fn smoke(#[case] message: RDCleanPathPdu) {
let encoded = message.to_der().unwrap();
let decoded = RDCleanPathPdu::from_der(&encoded).unwrap();
assert_eq!(message, decoded);
}
macro_rules! assert_serialization {
($left:expr, $right:expr) => {{
if $left != $right {
let left = hex::encode(&$left);
let right = hex::encode(&$right);
let comparison = pretty_assertions::StrComparison::new(&left, &right);
panic!(
"assertion failed: `({} == {})`\n\n{comparison}",
stringify!($left),
stringify!($right),
);
}
}};
}
#[rstest]
#[case(request(), REQUEST_DER)]
#[case(response_success(), RESPONSE_SUCCESS_DER)]
#[case(response_http_error(), RESPONSE_HTTP_ERROR_DER)]
#[case(response_tls_error(), RESPONSE_TLS_ERROR_DER)]
fn serialization(#[case] message: RDCleanPathPdu, #[case] expected_der: &[u8]) {
let encoded = message.to_der().unwrap();
assert_serialization!(encoded, expected_der);
}
#[rstest]
#[case(REQUEST_DER)]
#[case(RESPONSE_SUCCESS_DER)]
#[case(RESPONSE_HTTP_ERROR_DER)]
#[case(RESPONSE_TLS_ERROR_DER)]
fn detect(#[case] der: &[u8]) {
let result = RDCleanPathPdu::detect(der);
let DetectionResult::Detected { version: detected_version, total_length: detected_length } = result else {
panic!("unexpected result: {result:?}");
};
assert_eq!(detected_version, VERSION_1);
assert_eq!(detected_length, der.len());
}
#[rstest]
#[case(&[])]
#[case(&[0x30])]
#[case(&[0x30, 0x15])]
#[case(&[0x30, 0x15, 0xA0])]
#[case(&[0x30, 0x32, 0xA0, 0x4])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2])]
#[case(&[0x30, 0x32, 0xA0, 0x4, 0x2, 0x2, 0xD])]
fn detect_not_enough(#[case] payload: &[u8]) {
let result = RDCleanPathPdu::detect(payload);
assert_eq!(result, DetectionResult::NotEnoughBytes);
}
}