Skip to content

Commit dd221bf

Browse files
probakowskiCBenoit
andauthored
feat: support license caching (#634)
Adds support for license caching by storing the license obtained from SERVER_UPGRADE_LICENSE message and sending CLIENT_LICENSE_INFO if a license requested by the server is already stored in the cache. Co-authored-by: Benoît Cortier <[email protected]>
1 parent a2378ef commit dd221bf

File tree

16 files changed

+424
-100
lines changed

16 files changed

+424
-100
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-client/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ironrdp = { workspace = true, features = [
4141
"rdpsnd",
4242
"cliprdr",
4343
"displaycontrol",
44+
"connector"
4445
] }
4546
ironrdp-cliprdr-native.workspace = true
4647
ironrdp-rdpsnd-native.workspace = true
@@ -77,6 +78,7 @@ reqwest = "0.12"
7778
url = "2.5"
7879
raw-window-handle = "0.6.2"
7980
ironrdp-core = { workspace = true, features = ["alloc"] }
81+
uuid = { version = "1.11.0"}
8082

8183
[target.'cfg(windows)'.dependencies]
8284
windows = { workspace = true, features = ["Win32_Foundation"] }

crates/ironrdp-client/src/config.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
use core::num::ParseIntError;
2-
use core::str::FromStr;
3-
use std::io;
4-
51
use anyhow::Context as _;
62
use clap::clap_derive::ValueEnum;
73
use clap::Parser;
4+
use core::num::ParseIntError;
5+
use core::str::FromStr;
86
use ironrdp::connector::{self, Credentials};
97
use ironrdp::pdu::rdp::capability_sets::MajorPlatformType;
108
use ironrdp::pdu::rdp::client_info::PerformanceFlags;
9+
use std::io;
1110
use tap::prelude::*;
1211

1312
const DEFAULT_WIDTH: u16 = 1920;
@@ -316,6 +315,8 @@ impl Config {
316315
whoami::Platform::Android => MajorPlatformType::ANDROID,
317316
_ => MajorPlatformType::UNSPECIFIED,
318317
},
318+
hardware_id: None,
319+
license_cache: None,
319320
no_server_pointer: args.no_server_pointer,
320321
autologon: args.autologon,
321322
request_data: None,

crates/ironrdp-connector/src/connection.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
use core::mem;
2-
use std::borrow::Cow;
3-
use std::net::SocketAddr;
4-
52
use ironrdp_core::{decode, encode_vec, Encode, WriteBuf};
63
use ironrdp_pdu::rdp::client_info::{OptionalSystemTime, TimezoneInfo};
74
use ironrdp_pdu::x224::X224;
85
use ironrdp_pdu::{gcc, mcs, nego, rdp, PduHint};
96
use ironrdp_svc::{StaticChannelSet, StaticVirtualChannel, SvcClientProcessor};
7+
use std::borrow::Cow;
8+
use std::net::SocketAddr;
9+
use std::sync::Arc;
1010

1111
use crate::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
1212
use crate::connection_activation::{ConnectionActivationSequence, ConnectionActivationState};
13-
use crate::license_exchange::LicenseExchangeSequence;
13+
use crate::license_exchange::{LicenseExchangeSequence, NoopLicenseCache};
1414
use crate::{
1515
encode_x224_packet, Config, ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, Sequence, State,
1616
Written,
@@ -481,6 +481,11 @@ impl Sequence for ClientConnector {
481481
io_channel_id,
482482
self.config.credentials.username().unwrap_or("").to_owned(),
483483
self.config.domain.clone(),
484+
self.config.hardware_id.unwrap_or_default(),
485+
self.config
486+
.license_cache
487+
.clone()
488+
.unwrap_or_else(|| Arc::new(NoopLicenseCache)),
484489
),
485490
},
486491
),

crates/ironrdp-connector/src/lib.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ pub mod credssp;
1717
mod license_exchange;
1818
mod server_name;
1919

20-
use core::any::Any;
21-
use core::fmt;
22-
20+
pub use crate::license_exchange::LicenseCache;
2321
pub use channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
2422
pub use connection::{encode_send_data_request, ClientConnector, ClientConnectorState, ConnectionResult};
2523
pub use connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState};
24+
use core::any::Any;
25+
use core::fmt;
2626
use ironrdp_core::{encode_buf, encode_vec, Encode, WriteBuf};
2727
use ironrdp_pdu::nego::NegoRequestData;
2828
use ironrdp_pdu::rdp::capability_sets;
@@ -32,6 +32,7 @@ use ironrdp_pdu::{gcc, x224, PduHint};
3232
pub use license_exchange::{LicenseExchangeSequence, LicenseExchangeState};
3333
pub use server_name::ServerName;
3434
pub use sspi;
35+
use std::sync::Arc;
3536

3637
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3738
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
@@ -161,6 +162,10 @@ pub struct Config {
161162
pub dig_product_id: String,
162163
pub client_dir: String,
163164
pub platform: capability_sets::MajorPlatformType,
165+
/// Unique identifier for the computer
166+
///
167+
/// Each 32-bit integer contains client hardware-specific data helping the server uniquely identify the client.
168+
pub hardware_id: Option<[u32; 4]>,
164169
/// Optional data for the x224 connection request.
165170
///
166171
/// Fallbacks to a sensible default depending on the provided credentials:
@@ -170,6 +175,7 @@ pub struct Config {
170175
pub request_data: Option<NegoRequestData>,
171176
/// If true, the INFO_AUTOLOGON flag is set in the [`ClientInfoPdu`](ironrdp_pdu::rdp::ClientInfoPdu)
172177
pub autologon: bool,
178+
pub license_cache: Option<Arc<dyn LicenseCache>>,
173179

174180
// FIXME(@CBenoit): these are client-only options, not part of the connector.
175181
pub no_server_pointer: bool,

crates/ironrdp-connector/src/license_exchange.rs

+133-48
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
use super::{legacy, ConnectorError, ConnectorErrorExt};
2+
use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written};
3+
use core::fmt::Debug;
14
use core::{fmt, mem};
2-
35
use ironrdp_core::WriteBuf;
4-
use ironrdp_pdu::rdp::server_license::{self, LicensePdu, ServerLicenseError};
6+
use ironrdp_pdu::rdp::server_license::{self, LicenseInformation, LicensePdu, ServerLicenseError};
57
use ironrdp_pdu::PduHint;
68
use rand_core::{OsRng, RngCore as _};
7-
8-
use super::legacy;
9-
use crate::{encode_send_data_request, ConnectorResult, ConnectorResultExt as _, Sequence, State, Written};
9+
use std::str;
10+
use std::sync::Arc;
1011

1112
#[derive(Default, Debug)]
1213
#[non_exhaustive]
@@ -57,15 +58,43 @@ pub struct LicenseExchangeSequence {
5758
pub io_channel_id: u16,
5859
pub username: String,
5960
pub domain: Option<String>,
61+
pub hardware_id: [u32; 4],
62+
pub license_cache: Arc<dyn LicenseCache>,
63+
}
64+
65+
pub trait LicenseCache: Sync + Send + Debug {
66+
fn get_license(&self, license_info: LicenseInformation) -> ConnectorResult<Option<Vec<u8>>>;
67+
fn store_license(&self, license_info: LicenseInformation) -> ConnectorResult<()>;
68+
}
69+
70+
#[derive(Debug)]
71+
pub(crate) struct NoopLicenseCache;
72+
73+
impl LicenseCache for NoopLicenseCache {
74+
fn get_license(&self, _license_info: LicenseInformation) -> ConnectorResult<Option<Vec<u8>>> {
75+
Ok(None)
76+
}
77+
78+
fn store_license(&self, _license_info: LicenseInformation) -> ConnectorResult<()> {
79+
Ok(())
80+
}
6081
}
6182

6283
impl LicenseExchangeSequence {
63-
pub fn new(io_channel_id: u16, username: String, domain: Option<String>) -> Self {
84+
pub fn new(
85+
io_channel_id: u16,
86+
username: String,
87+
domain: Option<String>,
88+
hardware_id: [u32; 4],
89+
license_cache: Arc<dyn LicenseCache>,
90+
) -> Self {
6491
Self {
6592
state: LicenseExchangeState::NewLicenseRequest,
6693
io_channel_id,
6794
username,
6895
domain,
96+
hardware_id,
97+
license_cache,
6998
}
7099
}
71100
}
@@ -107,52 +136,102 @@ impl Sequence for LicenseExchangeSequence {
107136
let mut premaster_secret = [0u8; server_license::PREMASTER_SECRET_SIZE];
108137
OsRng.fill_bytes(&mut premaster_secret);
109138

110-
match server_license::ClientNewLicenseRequest::from_server_license_request(
111-
&license_request,
112-
&client_random,
113-
&premaster_secret,
114-
&self.username,
115-
self.domain.as_deref().unwrap_or(""),
116-
) {
117-
Ok((new_license_request, encryption_data)) => {
118-
trace!(?encryption_data, "Successfully generated Client New License Request");
119-
info!(message = ?new_license_request, "Send");
120-
121-
let written = encode_send_data_request::<LicensePdu>(
122-
send_data_indication_ctx.initiator_id,
123-
send_data_indication_ctx.channel_id,
124-
&new_license_request.into(),
125-
output,
126-
)?;
127-
128-
(
129-
Written::from_size(written)?,
130-
LicenseExchangeState::PlatformChallenge { encryption_data },
131-
)
139+
let license_info = license_request
140+
.scope_list
141+
.iter()
142+
.filter_map(|scope| {
143+
self.license_cache
144+
.get_license(LicenseInformation {
145+
version: license_request.product_info.version,
146+
scope: scope.0.clone(),
147+
company_name: license_request.product_info.company_name.clone(),
148+
product_id: license_request.product_info.product_id.clone(),
149+
license_info: vec![],
150+
})
151+
.transpose()
152+
})
153+
.next()
154+
.transpose()?;
155+
156+
if let Some(info) = license_info {
157+
match server_license::ClientLicenseInfo::from_server_license_request(
158+
&license_request,
159+
&client_random,
160+
&premaster_secret,
161+
self.hardware_id,
162+
info,
163+
) {
164+
Ok((client_license_info, encryption_data)) => {
165+
trace!(?encryption_data, "Successfully generated Client License Info");
166+
info!(message = ?client_license_info, "Send");
167+
168+
let written = encode_send_data_request::<LicensePdu>(
169+
send_data_indication_ctx.initiator_id,
170+
send_data_indication_ctx.channel_id,
171+
&client_license_info.into(),
172+
output,
173+
)?;
174+
175+
trace!(?written, "Written ClientLicenseInfo");
176+
177+
(
178+
Written::from_size(written)?,
179+
LicenseExchangeState::PlatformChallenge { encryption_data },
180+
)
181+
}
182+
Err(err) => {
183+
return Err(custom_err!("ClientNewLicenseRequest", err));
184+
}
132185
}
133-
Err(error) => {
134-
if let ServerLicenseError::InvalidX509Certificate {
135-
source: error,
136-
cert_der,
137-
} = &error
138-
{
139-
struct BytesHexFormatter<'a>(&'a [u8]);
140-
141-
impl fmt::Display for BytesHexFormatter<'_> {
142-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143-
write!(f, "0x")?;
144-
self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}"))
186+
} else {
187+
let hwid = self.hardware_id;
188+
match server_license::ClientNewLicenseRequest::from_server_license_request(
189+
&license_request,
190+
&client_random,
191+
&premaster_secret,
192+
&self.username,
193+
&format!("{:X}-{:X}-{:X}-{:X}", hwid[0], hwid[1], hwid[2], hwid[3]),
194+
) {
195+
Ok((new_license_request, encryption_data)) => {
196+
trace!(?encryption_data, "Successfully generated Client New License Request");
197+
info!(message = ?new_license_request, "Send");
198+
199+
let written = encode_send_data_request::<LicensePdu>(
200+
send_data_indication_ctx.initiator_id,
201+
send_data_indication_ctx.channel_id,
202+
&new_license_request.into(),
203+
output,
204+
)?;
205+
206+
(
207+
Written::from_size(written)?,
208+
LicenseExchangeState::PlatformChallenge { encryption_data },
209+
)
210+
}
211+
Err(error) => {
212+
if let ServerLicenseError::InvalidX509Certificate {
213+
source: error,
214+
cert_der,
215+
} = &error
216+
{
217+
struct BytesHexFormatter<'a>(&'a [u8]);
218+
219+
impl fmt::Display for BytesHexFormatter<'_> {
220+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221+
write!(f, "0x")?;
222+
self.0.iter().try_for_each(|byte| write!(f, "{byte:02X}"))
223+
}
145224
}
225+
226+
error!(
227+
%error,
228+
cert_der = %BytesHexFormatter(cert_der),
229+
"Unsupported or invalid X509 certificate received during license exchange step"
230+
);
146231
}
147232

148-
error!(
149-
%error,
150-
cert_der = %BytesHexFormatter(cert_der),
151-
"Unsupported or invalid X509 certificate received during license exchange step"
152-
);
233+
return Err(custom_err!("ClientNewLicenseRequest", error));
153234
}
154-
155-
return Err(custom_err!("ClientNewLicenseRequest", error));
156235
}
157236
}
158237
}
@@ -188,7 +267,7 @@ impl Sequence for LicenseExchangeSequence {
188267
let challenge_response =
189268
server_license::ClientPlatformChallengeResponse::from_server_platform_challenge(
190269
&challenge,
191-
self.domain.as_deref().unwrap_or(""),
270+
self.hardware_id,
192271
&encryption_data,
193272
)
194273
.map_err(|e| custom_err!("ClientPlatformChallengeResponse", e))?;
@@ -242,6 +321,12 @@ impl Sequence for LicenseExchangeSequence {
242321
.map_err(|e| custom_err!("license verification", e))?;
243322

244323
debug!("License verified with success");
324+
325+
let license_info = upgrade_license
326+
.new_license_info(&encryption_data)
327+
.map_err(ConnectorError::decode)?;
328+
329+
self.license_cache.store_license(license_info)?
245330
}
246331
LicensePdu::LicensingErrorMessage(error_message) => {
247332
if error_message.error_code != server_license::LicenseErrorCode::StatusValidClient {

0 commit comments

Comments
 (0)