feat(core): RDP client on IronRDP
clients::rdp connects over any caller-dialled TCP stream (direct, proxy or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM), optionally falling back to TLS-only logon for servers without NLA such as xrdp (credentials are passed so they log straight in). The server certificate is pinned through a caller check on its SHA256 fingerprint before any credentials are sent. The desktop decodes into the VNC client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse operations), Display Control resizes and the deactivation-reactivation sequence run on the session thread. IronRDP's releases need three vendored patches (vendor/README.md): picky and sspi with their RustCrypto release-candidate pins relaxed so they share russh's stable versions (their own suites, 402 tests, pass against them), and ironrdp-session with two unreleased upstream bitmap fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops render sheared. Verified live against xrdp (scottyhardy/docker-remote-desktop): logon, full desktop rendering, keyboard and mouse input.
This commit is contained in:
Vendored
+179
@@ -0,0 +1,179 @@
|
||||
// Based on the SSPI client example from MSDN: https://docs.microsoft.com/en-us/windows/win32/secauthn/using-sspi-with-a-windows-sockets-client
|
||||
|
||||
// This example works with the server example using SSPI. The client and server examples are designed to work together.
|
||||
// This example demonstrates initializing an authenticating SSPI session with the NTLM, connecting with a server, establishing
|
||||
// a secure communication session, receiving and decrypting a message from the server within the secure session.
|
||||
|
||||
use std::io;
|
||||
use std::net::TcpStream;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use sspi::{
|
||||
AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, DataRepresentation, Ntlm, SecurityBuffer,
|
||||
SecurityBufferRef, SecurityStatus, Sspi, SspiImpl, Username,
|
||||
};
|
||||
|
||||
const IP: &str = "127.0.0.1:8080";
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let mut stream = TcpStream::connect(IP).expect("Failed to connect to the server");
|
||||
|
||||
println!("Connected to the server.");
|
||||
|
||||
let mut ntlm = Ntlm::new();
|
||||
|
||||
let account_name = whoami::username().unwrap();
|
||||
let computer_name = whoami::hostname().unwrap();
|
||||
let username = Username::new(&account_name, Some(&computer_name)).map_err(io::Error::other)?;
|
||||
|
||||
let identity = AuthIdentity {
|
||||
username,
|
||||
password: String::from("password").into(),
|
||||
};
|
||||
|
||||
do_authentication(&mut ntlm, &identity, &mut stream).expect("Failed to authenticate connection");
|
||||
println!("Authenticated!");
|
||||
|
||||
let mut trailer = Vec::new();
|
||||
|
||||
// By agreement, the server encrypted the message and set the size
|
||||
// of the trailer block to be just what it needed. decrypt_message
|
||||
// needs the size of the trailer block.
|
||||
//
|
||||
// By agreement, the server placed the trailer at the beginning
|
||||
// of the message, and the data comes after the trailer.
|
||||
println!("Receiving the trailer...");
|
||||
read_message(&mut stream, &mut trailer)?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
println!("Receiving the data...");
|
||||
read_message(&mut stream, &mut data)?;
|
||||
|
||||
println!("Encrypted message: {data:?}");
|
||||
|
||||
let mut msg_buffer = vec![
|
||||
SecurityBufferRef::token_buf(&mut trailer),
|
||||
SecurityBufferRef::data_buf(&mut data),
|
||||
];
|
||||
|
||||
let _decryption_flags = ntlm.decrypt_message(&mut msg_buffer)?;
|
||||
|
||||
println!("Decrypting...");
|
||||
println!(
|
||||
"Decrypted message: [{}]",
|
||||
std::str::from_utf8(msg_buffer[1].data()).unwrap()
|
||||
);
|
||||
|
||||
println!("Communication successfully finished.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_authentication(ntlm: &mut Ntlm, identity: &AuthIdentity, mut stream: &mut TcpStream) -> Result<(), io::Error> {
|
||||
let mut acq_cred_result = ntlm
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(CredentialUse::Outbound)
|
||||
.with_auth_data(identity)
|
||||
.execute(ntlm)?;
|
||||
|
||||
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
let username = whoami::username().unwrap();
|
||||
|
||||
let mut builder = ntlm
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(username.as_str())
|
||||
.with_output(&mut output_buffer);
|
||||
|
||||
let _result = ntlm
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_to_result()?;
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
loop {
|
||||
output_buffer[0].buffer.clear();
|
||||
|
||||
read_message(&mut stream, &mut input_buffer[0].buffer)?;
|
||||
|
||||
let mut builder = ntlm
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(username.as_str())
|
||||
.with_input(&mut input_buffer)
|
||||
.with_output(&mut output_buffer);
|
||||
|
||||
let result = ntlm
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_to_result()?;
|
||||
|
||||
if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
|
||||
println!("Completing the token...");
|
||||
ntlm.complete_auth_token(&mut output_buffer)?;
|
||||
}
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
input_buffer[0].buffer.clear();
|
||||
|
||||
if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is read from the first 2 bytes of the message in little-endian order.
|
||||
pub fn read_message<T: io::Read, A: io::Write>(stream: &mut T, output_buffer: &mut A) -> Result<(), io::Error> {
|
||||
let msg_len = stream.read_u16::<LittleEndian>()?;
|
||||
|
||||
let mut buff = vec![0u8; msg_len as usize];
|
||||
stream.read_exact(&mut buff)?;
|
||||
|
||||
output_buffer.write_all(&buff)?;
|
||||
|
||||
println!("Received the buffer [{} bytes]: {:?}", buff.len(), buff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is written in the first 2 bytes of the message in little-endian order.
|
||||
pub fn write_message<T: io::Write>(stream: &mut T, input_buffer: &[u8]) -> Result<(), io::Error> {
|
||||
if !input_buffer.is_empty() {
|
||||
println!("Sending the buffer [{} bytes]: {:?}", input_buffer.len(), input_buffer);
|
||||
|
||||
stream.write_u16::<LittleEndian>(input_buffer.len() as u16)?;
|
||||
stream.write_all(input_buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_read_correctly() {
|
||||
let mut msg = vec![0x3, 0x0];
|
||||
msg.append(&mut b"abc".to_vec());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
read_message(&mut msg.as_slice(), &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(buffer, b"abc".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_written_correctly() {
|
||||
let mut msg = vec![9, 8, 7];
|
||||
let mut stream = Vec::new();
|
||||
|
||||
write_message(&mut stream, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(stream, vec![3, 0, 9, 8, 7]);
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
use std::error::Error;
|
||||
|
||||
use base64::Engine;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::{
|
||||
ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CONNECTION, CONTENT_LENGTH, HOST, USER_AGENT,
|
||||
WWW_AUTHENTICATE,
|
||||
};
|
||||
use sspi::{
|
||||
AcquireCredentialsHandleResult, BufferType, ClientRequestFlags, CredentialsBuffers, DataRepresentation,
|
||||
InitializeSecurityContextResult, Kerberos, KerberosConfig, SecurityBuffer, SecurityStatus, Sspi, SspiImpl,
|
||||
Username,
|
||||
};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let kdc_url = std::env::var("SSPI_KDC_URL").expect("missing KDC URL set in SSPI_KDC_URL"); //tcp://ad-compter-name.domain:88
|
||||
let hostname = std::env::var("SSPI_WINRM_HOST").expect("missing host name set in SSPI_WINRM_HOST"); // winrm_server_name.domain
|
||||
let username = std::env::var("SSPI_WINRM_USER").expect("missing username set in SSPI_WINRM_USER"); // username@domain
|
||||
let password = std::env::var("SSPI_WINRM_PASS").expect("missing password set in SSPI_WINRM_PASS");
|
||||
let auth_method = std::env::var("SSPI_WINRM_AUTH").expect("missing auth METHOD set in SSPI_WINRM_AUTH"); // Negotiate or Kerberos
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer())
|
||||
.with(EnvFilter::from_env("SSPI_LOG_LEVEL"))
|
||||
.init();
|
||||
|
||||
let kerberos_config = KerberosConfig::new(&kdc_url, hostname.clone());
|
||||
let mut kerberos = Kerberos::new_client_from_config(kerberos_config).unwrap();
|
||||
|
||||
let mut acq_creds_handle_result = get_cred_handle(&mut kerberos, username, password);
|
||||
|
||||
let mut input_token = String::new();
|
||||
let mut client = reqwest::blocking::Client::new(); // super IMPORTANT, KEEP-ALIVE the http connection!
|
||||
loop {
|
||||
let (output_token, status) = step(
|
||||
&mut kerberos,
|
||||
&mut acq_creds_handle_result.credentials_handle,
|
||||
&input_token,
|
||||
&hostname,
|
||||
)?;
|
||||
|
||||
if status == SecurityStatus::ContinueNeeded || status == SecurityStatus::Ok {
|
||||
let (token_from_server, status_code) =
|
||||
process_authentication(&output_token, &mut client, &auth_method, &hostname)?;
|
||||
if status_code == StatusCode::OK {
|
||||
println!("authenticated");
|
||||
break Ok(());
|
||||
}
|
||||
input_token = token_from_server;
|
||||
} else {
|
||||
return Err("Having problem continue authentication".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_cred_handle(
|
||||
kerberos: &mut Kerberos,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> AcquireCredentialsHandleResult<Option<CredentialsBuffers>> {
|
||||
let identity = sspi::AuthIdentity {
|
||||
username: Username::parse(&username).expect("username is not in the correct format"),
|
||||
password: password.into(),
|
||||
};
|
||||
|
||||
kerberos
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(sspi::CredentialUse::Outbound)
|
||||
.with_auth_data(&identity.into())
|
||||
.execute(kerberos)
|
||||
.expect("AcquireCredentialsHandle resulted in error")
|
||||
}
|
||||
|
||||
pub(crate) fn process_authentication(
|
||||
token_neeeds_to_be_sent: &String,
|
||||
client: &mut reqwest::blocking::Client,
|
||||
auth_method: &str,
|
||||
hostname: &str,
|
||||
) -> Result<(String, StatusCode), Box<dyn Error + Send + Sync>> {
|
||||
let server_result = send_http(token_neeeds_to_be_sent, client, hostname, auth_method)?;
|
||||
if server_result.status() == StatusCode::OK {
|
||||
return Ok((String::new(), StatusCode::OK));
|
||||
}
|
||||
let www_authenticate = server_result
|
||||
.headers()
|
||||
.get(WWW_AUTHENTICATE)
|
||||
.ok_or("expecting www-authentication header from server but not found")?;
|
||||
let server_token = www_authenticate
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace(format!("{auth_method} ").as_str(), "");
|
||||
Ok((server_token, server_result.status()))
|
||||
}
|
||||
|
||||
pub(crate) fn send_http(
|
||||
negotiate_token: &String,
|
||||
client: &mut reqwest::blocking::Client,
|
||||
hostname: &str,
|
||||
auth_method: &str,
|
||||
) -> Result<reqwest::blocking::Response, Box<dyn Error + Send + Sync>> {
|
||||
let resp = client
|
||||
.post(format!("http://{hostname}:5985/wsman?PSVersion=7.3.8"))
|
||||
.header(AUTHORIZATION, format!("{auth_method} {negotiate_token}"))
|
||||
.header(HOST, format!("{hostname}:5985"))
|
||||
.header(CONNECTION, "keep-alive")
|
||||
.header(CONTENT_LENGTH, "0")
|
||||
.header(
|
||||
USER_AGENT,
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0",
|
||||
)
|
||||
.header(ACCEPT, "*/*")
|
||||
.header(ACCEPT_ENCODING, "gzip, deflate")
|
||||
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.9")
|
||||
.send()?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
fn step_helper(
|
||||
kerberos: &mut Kerberos,
|
||||
cred_handle: &mut <Kerberos as SspiImpl>::CredentialsHandle,
|
||||
input_buffer: &mut [SecurityBuffer],
|
||||
output_buffer: &mut [SecurityBuffer],
|
||||
hostname: &str,
|
||||
) -> Result<InitializeSecurityContextResult, Box<dyn Error + Send + Sync>> {
|
||||
let target_name = format!("HTTP/{hostname}");
|
||||
let mut builder = kerberos
|
||||
.initialize_security_context()
|
||||
.with_credentials_handle(cred_handle)
|
||||
.with_context_requirements(ClientRequestFlags::MUTUAL_AUTH)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_target_name(&target_name)
|
||||
.with_input(input_buffer)
|
||||
.with_output(output_buffer);
|
||||
|
||||
let result = kerberos
|
||||
.initialize_security_context_impl(&mut builder)?
|
||||
.resolve_with_default_network_client()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn step(
|
||||
kerberos: &mut Kerberos,
|
||||
cred_handle: &mut <Kerberos as SspiImpl>::CredentialsHandle,
|
||||
input_token: &String,
|
||||
hostname: &str,
|
||||
) -> Result<(String, SecurityStatus), Box<dyn Error + Send + Sync>> {
|
||||
let input_buffer = base64::engine::general_purpose::STANDARD.decode(input_token).unwrap();
|
||||
let mut secure_input_buffer = vec![SecurityBuffer::new(input_buffer, BufferType::Token)];
|
||||
let mut secure_output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
let result = step_helper(
|
||||
kerberos,
|
||||
cred_handle,
|
||||
&mut secure_input_buffer,
|
||||
&mut secure_output_buffer,
|
||||
hostname,
|
||||
)?;
|
||||
|
||||
let output_buffer = secure_output_buffer[0].to_owned();
|
||||
|
||||
Ok((
|
||||
base64::engine::general_purpose::STANDARD.encode(output_buffer.buffer),
|
||||
result.status,
|
||||
))
|
||||
}
|
||||
Vendored
+159
@@ -0,0 +1,159 @@
|
||||
// Based on the SSPI server example from MSDN: https://docs.microsoft.com/en-us/windows/win32/secauthn/using-sspi-with-a-windows-sockets-server
|
||||
|
||||
// This example works with the client example using SSPI.
|
||||
// It demonstrates how to connect with a client, establish a secure communication session, and send the client an encrypted message.
|
||||
|
||||
use std::io;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use sspi::{
|
||||
AuthIdentity, BufferType, CredentialUse, DataRepresentation, EncryptionFlags, Ntlm, SecurityBuffer,
|
||||
SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username,
|
||||
};
|
||||
|
||||
const IP: &str = "127.0.0.1:8080";
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let listener = TcpListener::bind(IP).expect("Unable to start the server");
|
||||
|
||||
println!("Started the server.");
|
||||
|
||||
let (mut stream, _client_addr) = listener.accept()?;
|
||||
|
||||
let mut ntlm = Ntlm::new();
|
||||
|
||||
let account_name = whoami::username().unwrap();
|
||||
let computer_name = whoami::hostname().unwrap();
|
||||
let username = Username::new(&account_name, Some(&computer_name)).map_err(io::Error::other)?;
|
||||
|
||||
let identity = AuthIdentity {
|
||||
username,
|
||||
password: String::from("password").into(),
|
||||
};
|
||||
|
||||
do_authentication(&mut ntlm, &identity, &mut stream).expect("Failed to authenticate connection");
|
||||
println!("Authenticated!");
|
||||
|
||||
println!("Sending the encrypted message...");
|
||||
|
||||
let msg = "This is your server speaking!".to_string();
|
||||
|
||||
// By agreement, the server encrypts and sets the size of the
|
||||
// trailer block to be just what it needed. decrypt_message
|
||||
// needs the size of the trailer block.
|
||||
//
|
||||
// By agreement, the server places the trailer at the beginning
|
||||
// of the message, and the data comes after the trailer.
|
||||
let mut token = vec![0u8; ntlm.query_context_sizes()?.security_trailer as usize];
|
||||
let mut data = msg.as_bytes().to_vec();
|
||||
let mut msg_buffer = vec![
|
||||
SecurityBufferRef::token_buf(token.as_mut_slice()),
|
||||
SecurityBufferRef::data_buf(data.as_mut_slice()),
|
||||
];
|
||||
|
||||
println!("Unencrypted message: [{msg}]");
|
||||
println!("Encrypting...");
|
||||
|
||||
let _result = ntlm.encrypt_message(EncryptionFlags::empty(), &mut msg_buffer)?;
|
||||
|
||||
println!("Encrypted message: {:?}", msg_buffer[1].data());
|
||||
|
||||
println!("Sending the trailer...");
|
||||
write_message(&mut stream, msg_buffer[0].data())?;
|
||||
|
||||
println!("Sending the data...");
|
||||
write_message(&mut stream, msg_buffer[1].data())?;
|
||||
|
||||
println!("Communication successfully finished.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_authentication(ntlm: &mut Ntlm, identity: &AuthIdentity, mut stream: &mut TcpStream) -> Result<(), sspi::Error> {
|
||||
let mut acq_cred_result = ntlm
|
||||
.acquire_credentials_handle()
|
||||
.with_credential_use(CredentialUse::Inbound)
|
||||
.with_auth_data(identity)
|
||||
.execute(ntlm)?;
|
||||
|
||||
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
|
||||
|
||||
loop {
|
||||
read_message(&mut stream, &mut input_buffer[0].buffer)?;
|
||||
|
||||
let builder = ntlm
|
||||
.accept_security_context()
|
||||
.with_credentials_handle(&mut acq_cred_result.credentials_handle)
|
||||
.with_context_requirements(ServerRequestFlags::ALLOCATE_MEMORY)
|
||||
.with_target_data_representation(DataRepresentation::Native)
|
||||
.with_input(&mut input_buffer)
|
||||
.with_output(&mut output_buffer);
|
||||
let result = ntlm.accept_security_context_impl(builder)?.resolve_to_result()?;
|
||||
|
||||
if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
|
||||
println!("Completing the token...");
|
||||
ntlm.complete_auth_token(&mut output_buffer)?;
|
||||
}
|
||||
|
||||
write_message(&mut stream, &output_buffer[0].buffer)?;
|
||||
|
||||
output_buffer[0].buffer.clear();
|
||||
input_buffer[0].buffer.clear();
|
||||
|
||||
if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is read from the first 2 bytes of the message in little-endian order.
|
||||
pub fn read_message<T: io::Read, A: io::Write>(stream: &mut T, output_buffer: &mut A) -> Result<(), io::Error> {
|
||||
let msg_len = stream.read_u16::<LittleEndian>()?;
|
||||
|
||||
let mut buff = vec![0u8; msg_len as usize];
|
||||
stream.read_exact(&mut buff)?;
|
||||
|
||||
output_buffer.write_all(&buff)?;
|
||||
|
||||
println!("Received the buffer [{} bytes]: {:?}", buff.len(), buff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By agreement, the message length is written in the first 2 bytes of the message in little-endian order.
|
||||
pub fn write_message<T: io::Write>(stream: &mut T, input_buffer: &[u8]) -> Result<(), io::Error> {
|
||||
if !input_buffer.is_empty() {
|
||||
println!("Sending the buffer [{} bytes]: {:?}", input_buffer.len(), input_buffer);
|
||||
|
||||
stream.write_u16::<LittleEndian>(input_buffer.len() as u16)?;
|
||||
stream.write_all(input_buffer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_read_correctly() {
|
||||
let mut msg = vec![0x3, 0x0];
|
||||
msg.append(&mut b"abc".to_vec());
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
read_message(&mut msg.as_slice(), &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(buffer, b"abc".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_written_correctly() {
|
||||
let mut msg = vec![9, 8, 7];
|
||||
let mut stream = Vec::new();
|
||||
|
||||
write_message(&mut stream, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(stream, vec![3, 0, 9, 8, 7]);
|
||||
}
|
||||
Reference in New Issue
Block a user