summaryrefslogtreecommitdiff
path: root/utils/tcp_connection
diff options
context:
space:
mode:
Diffstat (limited to 'utils/tcp_connection')
-rw-r--r--utils/tcp_connection/Cargo.toml28
-rw-r--r--utils/tcp_connection/src/error.rs122
-rw-r--r--utils/tcp_connection/src/instance.rs542
-rw-r--r--utils/tcp_connection/src/instance_challenge.rs311
-rw-r--r--utils/tcp_connection/src/lib.rs6
-rw-r--r--utils/tcp_connection/tcp_connection_test/Cargo.toml9
-rw-r--r--utils/tcp_connection/tcp_connection_test/res/image/test_transfer.pngbin0 -> 1001369 bytes
-rw-r--r--utils/tcp_connection/tcp_connection_test/res/key/test_key.pem13
-rw-r--r--utils/tcp_connection/tcp_connection_test/res/key/test_key_private.pem51
-rw-r--r--utils/tcp_connection/tcp_connection_test/res/key/wrong_key_private.pem52
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/lib.rs17
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_challenge.rs160
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_connection.rs78
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_file_transfer.rs94
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_msgpack.rs103
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_tcp_target_build.rs32
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_utils.rs4
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_utils/handle.rs11
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_utils/target.rs201
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_utils/target_configure.rs53
-rw-r--r--utils/tcp_connection/tcp_connection_test/src/test_utils/target_connection.rs89
21 files changed, 1976 insertions, 0 deletions
diff --git a/utils/tcp_connection/Cargo.toml b/utils/tcp_connection/Cargo.toml
new file mode 100644
index 0000000..da258be
--- /dev/null
+++ b/utils/tcp_connection/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "tcp_connection"
+edition = "2024"
+version.workspace = true
+
+[dependencies]
+tokio = { version = "1.48.0", features = ["full"] }
+
+# Serialization
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.145"
+rmp-serde = "1.3.0"
+
+# Error handling
+thiserror = "2.0.17"
+
+# Uuid & Random
+uuid = "1.18.1"
+
+# Crypto
+rsa = { version = "0.9", features = ["pkcs5", "sha2"] }
+ed25519-dalek = "3.0.0-pre.1"
+ring = "0.17.14"
+rand = "0.10.0-rc.0"
+base64 = "0.22.1"
+pem = "3.0.6"
+crc = "3.3.0"
+blake3 = "1.8.2"
diff --git a/utils/tcp_connection/src/error.rs b/utils/tcp_connection/src/error.rs
new file mode 100644
index 0000000..32d06cc
--- /dev/null
+++ b/utils/tcp_connection/src/error.rs
@@ -0,0 +1,122 @@
+use std::io;
+use thiserror::Error;
+
+#[derive(Error, Debug, Clone)]
+pub enum TcpTargetError {
+ #[error("Authentication failed: {0}")]
+ Authentication(String),
+
+ #[error("Reference sheet not allowed: {0}")]
+ ReferenceSheetNotAllowed(String),
+
+ #[error("Cryptographic error: {0}")]
+ Crypto(String),
+
+ #[error("File operation error: {0}")]
+ File(String),
+
+ #[error("I/O error: {0}")]
+ Io(String),
+
+ #[error("Invalid configuration: {0}")]
+ Config(String),
+
+ #[error("Locked: {0}")]
+ Locked(String),
+
+ #[error("Network error: {0}")]
+ Network(String),
+
+ #[error("No result: {0}")]
+ NoResult(String),
+
+ #[error("Not found: {0}")]
+ NotFound(String),
+
+ #[error("Not local machine: {0}")]
+ NotLocal(String),
+
+ #[error("Not remote machine: {0}")]
+ NotRemote(String),
+
+ #[error("Pool already exists: {0}")]
+ PoolAlreadyExists(String),
+
+ #[error("Protocol error: {0}")]
+ Protocol(String),
+
+ #[error("Serialization error: {0}")]
+ Serialization(String),
+
+ #[error("Timeout: {0}")]
+ Timeout(String),
+
+ #[error("Unsupported operation: {0}")]
+ Unsupported(String),
+}
+
+impl From<io::Error> for TcpTargetError {
+ fn from(error: io::Error) -> Self {
+ TcpTargetError::Io(error.to_string())
+ }
+}
+
+impl From<serde_json::Error> for TcpTargetError {
+ fn from(error: serde_json::Error) -> Self {
+ TcpTargetError::Serialization(error.to_string())
+ }
+}
+
+impl From<&str> for TcpTargetError {
+ fn from(value: &str) -> Self {
+ TcpTargetError::Protocol(value.to_string())
+ }
+}
+
+impl From<String> for TcpTargetError {
+ fn from(value: String) -> Self {
+ TcpTargetError::Protocol(value)
+ }
+}
+
+impl From<rsa::errors::Error> for TcpTargetError {
+ fn from(error: rsa::errors::Error) -> Self {
+ TcpTargetError::Crypto(error.to_string())
+ }
+}
+
+impl From<ed25519_dalek::SignatureError> for TcpTargetError {
+ fn from(error: ed25519_dalek::SignatureError) -> Self {
+ TcpTargetError::Crypto(error.to_string())
+ }
+}
+
+impl From<ring::error::Unspecified> for TcpTargetError {
+ fn from(error: ring::error::Unspecified) -> Self {
+ TcpTargetError::Crypto(error.to_string())
+ }
+}
+
+impl From<base64::DecodeError> for TcpTargetError {
+ fn from(error: base64::DecodeError) -> Self {
+ TcpTargetError::Serialization(error.to_string())
+ }
+}
+
+impl From<pem::PemError> for TcpTargetError {
+ fn from(error: pem::PemError) -> Self {
+ TcpTargetError::Crypto(error.to_string())
+ }
+}
+
+impl From<rmp_serde::encode::Error> for TcpTargetError {
+ fn from(error: rmp_serde::encode::Error) -> Self {
+ TcpTargetError::Serialization(error.to_string())
+ }
+}
+
+impl From<rmp_serde::decode::Error> for TcpTargetError {
+ fn from(error: rmp_serde::decode::Error) -> Self {
+ TcpTargetError::Serialization(error.to_string())
+ }
+}
diff --git a/utils/tcp_connection/src/instance.rs b/utils/tcp_connection/src/instance.rs
new file mode 100644
index 0000000..8e6886c
--- /dev/null
+++ b/utils/tcp_connection/src/instance.rs
@@ -0,0 +1,542 @@
+use std::{path::Path, time::Duration};
+
+use serde::Serialize;
+use tokio::{
+ fs::{File, OpenOptions},
+ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
+ net::TcpStream,
+};
+
+use ring::signature::{self};
+
+use crate::error::TcpTargetError;
+
+const DEFAULT_CHUNK_SIZE: usize = 4096;
+const DEFAULT_TIMEOUT_SECS: u64 = 10;
+
+const ECDSA_P256_SHA256_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm =
+ &signature::ECDSA_P256_SHA256_ASN1_SIGNING;
+const ECDSA_P384_SHA384_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm =
+ &signature::ECDSA_P384_SHA384_ASN1_SIGNING;
+
+#[derive(Debug, Clone)]
+pub struct ConnectionConfig {
+ pub chunk_size: usize,
+ pub timeout_secs: u64,
+ pub enable_crc_validation: bool,
+}
+
+impl Default for ConnectionConfig {
+ fn default() -> Self {
+ Self {
+ chunk_size: DEFAULT_CHUNK_SIZE,
+ timeout_secs: DEFAULT_TIMEOUT_SECS,
+ enable_crc_validation: false,
+ }
+ }
+}
+
+pub struct ConnectionInstance {
+ pub(crate) stream: TcpStream,
+ config: ConnectionConfig,
+}
+
+impl From<TcpStream> for ConnectionInstance {
+ fn from(stream: TcpStream) -> Self {
+ Self {
+ stream,
+ config: ConnectionConfig::default(),
+ }
+ }
+}
+
+impl ConnectionInstance {
+ /// Create a new ConnectionInstance with custom configuration
+ pub fn with_config(stream: TcpStream, config: ConnectionConfig) -> Self {
+ Self { stream, config }
+ }
+
+ /// Get a reference to the current configuration
+ pub fn config(&self) -> &ConnectionConfig {
+ &self.config
+ }
+
+ /// Get a mutable reference to the current configuration
+ pub fn config_mut(&mut self) -> &mut ConnectionConfig {
+ &mut self.config
+ }
+ /// Serialize data and write to the target machine
+ pub async fn write<Data>(&mut self, data: Data) -> Result<(), TcpTargetError>
+ where
+ Data: Default + Serialize,
+ {
+ let Ok(json_text) = serde_json::to_string(&data) else {
+ return Err(TcpTargetError::Serialization(
+ "Serialize failed.".to_string(),
+ ));
+ };
+ Self::write_text(self, json_text).await?;
+ Ok(())
+ }
+
+ /// Serialize data to MessagePack and write to the target machine
+ pub async fn write_msgpack<Data>(&mut self, data: Data) -> Result<(), TcpTargetError>
+ where
+ Data: Serialize,
+ {
+ let msgpack_data = rmp_serde::to_vec(&data)?;
+ let len = msgpack_data.len() as u32;
+
+ self.stream.write_all(&len.to_be_bytes()).await?;
+ self.stream.write_all(&msgpack_data).await?;
+ Ok(())
+ }
+
+ /// Read data from target machine and deserialize from MessagePack
+ pub async fn read_msgpack<Data>(&mut self) -> Result<Data, TcpTargetError>
+ where
+ Data: serde::de::DeserializeOwned,
+ {
+ let mut len_buf = [0u8; 4];
+ self.stream.read_exact(&mut len_buf).await?;
+ let len = u32::from_be_bytes(len_buf) as usize;
+
+ let mut buffer = vec![0; len];
+ self.stream.read_exact(&mut buffer).await?;
+
+ let data = rmp_serde::from_slice(&buffer)?;
+ Ok(data)
+ }
+
+ /// Read data from target machine and deserialize
+ pub async fn read<Data>(&mut self) -> Result<Data, TcpTargetError>
+ where
+ Data: Default + serde::de::DeserializeOwned,
+ {
+ let Ok(json_text) = Self::read_text(self).await else {
+ return Err(TcpTargetError::Io("Read failed.".to_string()));
+ };
+ let Ok(deser_obj) = serde_json::from_str::<Data>(&json_text) else {
+ return Err(TcpTargetError::Serialization(
+ "Deserialize failed.".to_string(),
+ ));
+ };
+ Ok(deser_obj)
+ }
+
+ /// Serialize data and write to the target machine
+ pub async fn write_large<Data>(&mut self, data: Data) -> Result<(), TcpTargetError>
+ where
+ Data: Default + Serialize,
+ {
+ let Ok(json_text) = serde_json::to_string(&data) else {
+ return Err(TcpTargetError::Serialization(
+ "Serialize failed.".to_string(),
+ ));
+ };
+ Self::write_large_text(self, json_text).await?;
+ Ok(())
+ }
+
+ /// Read data from target machine and deserialize
+ pub async fn read_large<Data>(
+ &mut self,
+ buffer_size: impl Into<u32>,
+ ) -> Result<Data, TcpTargetError>
+ where
+ Data: Default + serde::de::DeserializeOwned,
+ {
+ let Ok(json_text) = Self::read_large_text(self, buffer_size).await else {
+ return Err(TcpTargetError::Io("Read failed.".to_string()));
+ };
+ let Ok(deser_obj) = serde_json::from_str::<Data>(&json_text) else {
+ return Err(TcpTargetError::Serialization(
+ "Deserialize failed.".to_string(),
+ ));
+ };
+ Ok(deser_obj)
+ }
+
+ /// Write text to the target machine
+ pub async fn write_text(&mut self, text: impl Into<String>) -> Result<(), TcpTargetError> {
+ let text = text.into();
+ let bytes = text.as_bytes();
+ let len = bytes.len() as u32;
+
+ self.stream.write_all(&len.to_be_bytes()).await?;
+ match self.stream.write_all(bytes).await {
+ Ok(_) => Ok(()),
+ Err(err) => Err(TcpTargetError::Io(err.to_string())),
+ }
+ }
+
+ /// Read text from the target machine
+ pub async fn read_text(&mut self) -> Result<String, TcpTargetError> {
+ let mut len_buf = [0u8; 4];
+ self.stream.read_exact(&mut len_buf).await?;
+ let len = u32::from_be_bytes(len_buf) as usize;
+
+ let mut buffer = vec![0; len];
+ self.stream.read_exact(&mut buffer).await?;
+
+ match String::from_utf8(buffer) {
+ Ok(text) => Ok(text),
+ Err(err) => Err(TcpTargetError::Serialization(format!(
+ "Invalid UTF-8 sequence: {}",
+ err
+ ))),
+ }
+ }
+
+ /// Write large text to the target machine (chunked)
+ pub async fn write_large_text(
+ &mut self,
+ text: impl Into<String>,
+ ) -> Result<(), TcpTargetError> {
+ let text = text.into();
+ let bytes = text.as_bytes();
+ let mut offset = 0;
+
+ while offset < bytes.len() {
+ let chunk = &bytes[offset..];
+ let written = match self.stream.write(chunk).await {
+ Ok(n) => n,
+ Err(err) => return Err(TcpTargetError::Io(err.to_string())),
+ };
+ offset += written;
+ }
+
+ Ok(())
+ }
+
+ /// Read large text from the target machine (chunked)
+ pub async fn read_large_text(
+ &mut self,
+ chunk_size: impl Into<u32>,
+ ) -> Result<String, TcpTargetError> {
+ let chunk_size = chunk_size.into() as usize;
+ let mut buffer = Vec::new();
+ let mut chunk_buf = vec![0; chunk_size];
+
+ loop {
+ match self.stream.read(&mut chunk_buf).await {
+ Ok(0) => break, // EOF
+ Ok(n) => {
+ buffer.extend_from_slice(&chunk_buf[..n]);
+ }
+ Err(err) => return Err(TcpTargetError::Io(err.to_string())),
+ }
+ }
+
+ Ok(String::from_utf8_lossy(&buffer).to_string())
+ }
+
+ /// Write large MessagePack data to the target machine (chunked)
+ pub async fn write_large_msgpack<Data>(
+ &mut self,
+ data: Data,
+ chunk_size: impl Into<u32>,
+ ) -> Result<(), TcpTargetError>
+ where
+ Data: Serialize,
+ {
+ let msgpack_data = rmp_serde::to_vec(&data)?;
+ let chunk_size = chunk_size.into() as usize;
+ let len = msgpack_data.len() as u32;
+
+ // Write total length first
+ self.stream.write_all(&len.to_be_bytes()).await?;
+
+ // Write data in chunks
+ let mut offset = 0;
+ while offset < msgpack_data.len() {
+ let end = std::cmp::min(offset + chunk_size, msgpack_data.len());
+ let chunk = &msgpack_data[offset..end];
+ match self.stream.write(chunk).await {
+ Ok(n) => offset += n,
+ Err(err) => return Err(TcpTargetError::Io(err.to_string())),
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Read large MessagePack data from the target machine (chunked)
+ pub async fn read_large_msgpack<Data>(
+ &mut self,
+ chunk_size: impl Into<u32>,
+ ) -> Result<Data, TcpTargetError>
+ where
+ Data: serde::de::DeserializeOwned,
+ {
+ let chunk_size = chunk_size.into() as usize;
+
+ // Read total length first
+ let mut len_buf = [0u8; 4];
+ self.stream.read_exact(&mut len_buf).await?;
+ let total_len = u32::from_be_bytes(len_buf) as usize;
+
+ // Read data in chunks
+ let mut buffer = Vec::with_capacity(total_len);
+ let mut remaining = total_len;
+ let mut chunk_buf = vec![0; chunk_size];
+
+ while remaining > 0 {
+ let read_size = std::cmp::min(chunk_size, remaining);
+ let chunk = &mut chunk_buf[..read_size];
+
+ match self.stream.read_exact(chunk).await {
+ Ok(_) => {
+ buffer.extend_from_slice(chunk);
+ remaining -= read_size;
+ }
+ Err(err) => return Err(TcpTargetError::Io(err.to_string())),
+ }
+ }
+
+ let data = rmp_serde::from_slice(&buffer)?;
+ Ok(data)
+ }
+
+ /// Write file to target machine.
+ pub async fn write_file(&mut self, file_path: impl AsRef<Path>) -> Result<(), TcpTargetError> {
+ let path = file_path.as_ref();
+
+ // Validate file
+ if !path.exists() {
+ return Err(TcpTargetError::File(format!(
+ "File not found: {}",
+ path.display()
+ )));
+ }
+ if path.is_dir() {
+ return Err(TcpTargetError::File(format!(
+ "Path is directory: {}",
+ path.display()
+ )));
+ }
+
+ // Open file and get metadata
+ let mut file = File::open(path).await?;
+ let file_size = file.metadata().await?.len();
+
+ // Send file header (version + size + crc)
+ self.stream.write_all(&1u64.to_be_bytes()).await?;
+ self.stream.write_all(&file_size.to_be_bytes()).await?;
+
+ // Calculate and send CRC32 if enabled
+ let file_crc = if self.config.enable_crc_validation {
+ let crc32 = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
+ let mut crc_calculator = crc32.digest();
+
+ let mut temp_reader =
+ BufReader::with_capacity(self.config.chunk_size, File::open(path).await?);
+ let mut temp_buffer = vec![0u8; self.config.chunk_size];
+ let mut temp_bytes_read = 0;
+
+ while temp_bytes_read < file_size {
+ let bytes_to_read =
+ (file_size - temp_bytes_read).min(self.config.chunk_size as u64) as usize;
+ temp_reader
+ .read_exact(&mut temp_buffer[..bytes_to_read])
+ .await?;
+ crc_calculator.update(&temp_buffer[..bytes_to_read]);
+ temp_bytes_read += bytes_to_read as u64;
+ }
+
+ crc_calculator.finalize()
+ } else {
+ 0
+ };
+
+ self.stream.write_all(&file_crc.to_be_bytes()).await?;
+
+ // If file size is 0, skip content transfer
+ if file_size == 0 {
+ self.stream.flush().await?;
+
+ // Wait for receiver confirmation
+ let mut ack = [0u8; 1];
+ tokio::time::timeout(
+ Duration::from_secs(self.config.timeout_secs),
+ self.stream.read_exact(&mut ack),
+ )
+ .await
+ .map_err(|_| TcpTargetError::Timeout("Ack timeout".to_string()))??;
+
+ if ack[0] != 1 {
+ return Err(TcpTargetError::Protocol(
+ "Receiver verification failed".to_string(),
+ ));
+ }
+
+ return Ok(());
+ }
+
+ // Transfer file content
+ let mut reader = BufReader::with_capacity(self.config.chunk_size, &mut file);
+ let mut bytes_sent = 0;
+
+ while bytes_sent < file_size {
+ let buffer = reader.fill_buf().await?;
+ if buffer.is_empty() {
+ break;
+ }
+
+ let chunk_size = buffer.len().min((file_size - bytes_sent) as usize);
+ self.stream.write_all(&buffer[..chunk_size]).await?;
+ reader.consume(chunk_size);
+
+ bytes_sent += chunk_size as u64;
+ }
+
+ // Verify transfer completion
+ if bytes_sent != file_size {
+ return Err(TcpTargetError::File(format!(
+ "Transfer incomplete: expected {} bytes, sent {} bytes",
+ file_size, bytes_sent
+ )));
+ }
+
+ self.stream.flush().await?;
+
+ // Wait for receiver confirmation
+ let mut ack = [0u8; 1];
+ tokio::time::timeout(
+ Duration::from_secs(self.config.timeout_secs),
+ self.stream.read_exact(&mut ack),
+ )
+ .await
+ .map_err(|_| TcpTargetError::Timeout("Ack timeout".to_string()))??;
+
+ if ack[0] != 1 {
+ return Err(TcpTargetError::Protocol(
+ "Receiver verification failed".to_string(),
+ ));
+ }
+
+ Ok(())
+ }
+
+ /// Read file from target machine
+ pub async fn read_file(&mut self, save_path: impl AsRef<Path>) -> Result<(), TcpTargetError> {
+ let path = save_path.as_ref();
+ // Create CRC instance at function scope to ensure proper lifetime
+ let crc_instance = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
+
+ // Make sure parent directory exists
+ if let Some(parent) = path.parent()
+ && !parent.exists()
+ {
+ tokio::fs::create_dir_all(parent).await?;
+ }
+
+ // Read file header (version + size + crc)
+ let mut version_buf = [0u8; 8];
+ self.stream.read_exact(&mut version_buf).await?;
+ let version = u64::from_be_bytes(version_buf);
+ if version != 1 {
+ return Err(TcpTargetError::Protocol(
+ "Unsupported transfer version".to_string(),
+ ));
+ }
+
+ let mut size_buf = [0u8; 8];
+ self.stream.read_exact(&mut size_buf).await?;
+ let file_size = u64::from_be_bytes(size_buf);
+
+ let mut expected_crc_buf = [0u8; 4];
+ self.stream.read_exact(&mut expected_crc_buf).await?;
+ let expected_crc = u32::from_be_bytes(expected_crc_buf);
+ if file_size == 0 {
+ // Create empty file and return early
+ let _file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(path)
+ .await?;
+ // Send confirmation
+ self.stream.write_all(&[1u8]).await?;
+ self.stream.flush().await?;
+ return Ok(());
+ }
+
+ // Prepare output file
+ let file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(path)
+ .await?;
+ let mut writer = BufWriter::with_capacity(self.config.chunk_size, file);
+
+ // Receive file content with CRC calculation if enabled
+ let mut bytes_received = 0;
+ let mut buffer = vec![0u8; self.config.chunk_size];
+ let mut crc_calculator = if self.config.enable_crc_validation {
+ Some(crc_instance.digest())
+ } else {
+ None
+ };
+
+ while bytes_received < file_size {
+ let bytes_to_read =
+ (file_size - bytes_received).min(self.config.chunk_size as u64) as usize;
+ let chunk = &mut buffer[..bytes_to_read];
+
+ self.stream.read_exact(chunk).await?;
+
+ writer.write_all(chunk).await?;
+
+ // Update CRC if validation is enabled
+ if let Some(ref mut crc) = crc_calculator {
+ crc.update(chunk);
+ }
+
+ bytes_received += bytes_to_read as u64;
+ }
+
+ // Verify transfer completion
+ if bytes_received != file_size {
+ return Err(TcpTargetError::File(format!(
+ "Transfer incomplete: expected {} bytes, received {} bytes",
+ file_size, bytes_received
+ )));
+ }
+
+ writer.flush().await?;
+
+ // Validate CRC if enabled
+ if self.config.enable_crc_validation
+ && let Some(crc_calculator) = crc_calculator
+ {
+ let actual_crc = crc_calculator.finalize();
+ if actual_crc != expected_crc && expected_crc != 0 {
+ return Err(TcpTargetError::File(format!(
+ "CRC validation failed: expected {:08x}, got {:08x}",
+ expected_crc, actual_crc
+ )));
+ }
+ }
+
+ // Final flush and sync
+ writer.flush().await?;
+ writer.into_inner().sync_all().await?;
+
+ // Verify completion
+ if bytes_received != file_size {
+ let _ = tokio::fs::remove_file(path).await;
+ return Err(TcpTargetError::File(format!(
+ "Transfer incomplete: expected {} bytes, received {} bytes",
+ file_size, bytes_received
+ )));
+ }
+
+ // Send confirmation
+ self.stream.write_all(&[1u8]).await?;
+ self.stream.flush().await?;
+
+ Ok(())
+ }
+}
diff --git a/utils/tcp_connection/src/instance_challenge.rs b/utils/tcp_connection/src/instance_challenge.rs
new file mode 100644
index 0000000..3a7f6a3
--- /dev/null
+++ b/utils/tcp_connection/src/instance_challenge.rs
@@ -0,0 +1,311 @@
+use std::path::Path;
+
+use rand::TryRngCore;
+use rsa::{
+ RsaPrivateKey, RsaPublicKey,
+ pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey},
+ sha2,
+};
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
+use ring::rand::SystemRandom;
+use ring::signature::{
+ self, ECDSA_P256_SHA256_ASN1, ECDSA_P384_SHA384_ASN1, EcdsaKeyPair, RSA_PKCS1_2048_8192_SHA256,
+ UnparsedPublicKey,
+};
+
+use crate::{error::TcpTargetError, instance::ConnectionInstance};
+
+const ECDSA_P256_SHA256_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm =
+ &signature::ECDSA_P256_SHA256_ASN1_SIGNING;
+const ECDSA_P384_SHA384_ASN1_SIGNING: &signature::EcdsaSigningAlgorithm =
+ &signature::ECDSA_P384_SHA384_ASN1_SIGNING;
+
+impl ConnectionInstance {
+ /// Initiates a challenge to the target machine to verify connection security
+ ///
+ /// This method performs a cryptographic challenge-response authentication:
+ /// 1. Generates a random 32-byte challenge
+ /// 2. Sends the challenge to the target machine
+ /// 3. Receives a digital signature of the challenge
+ /// 4. Verifies the signature using the appropriate public key
+ ///
+ /// # Arguments
+ /// * `public_key_dir` - Directory containing public key files for verification
+ ///
+ /// # Returns
+ /// * `Ok((true, "KeyId"))` - Challenge verification successful
+ /// * `Ok((false, "KeyId"))` - Challenge verification failed
+ /// * `Err(TcpTargetError)` - Error during challenge process
+ pub async fn challenge(
+ &mut self,
+ public_key_dir: impl AsRef<Path>,
+ ) -> Result<(bool, String), TcpTargetError> {
+ // Generate random challenge
+ let mut challenge = [0u8; 32];
+ rand::rngs::OsRng
+ .try_fill_bytes(&mut challenge)
+ .map_err(|e| {
+ TcpTargetError::Crypto(format!("Failed to generate random challenge: {}", e))
+ })?;
+
+ // Send challenge to target
+ self.stream.write_all(&challenge).await?;
+ self.stream.flush().await?;
+
+ // Read signature from target
+ let mut signature = Vec::new();
+ let mut signature_len_buf = [0u8; 4];
+ self.stream.read_exact(&mut signature_len_buf).await?;
+
+ let signature_len = u32::from_be_bytes(signature_len_buf) as usize;
+ signature.resize(signature_len, 0);
+ self.stream.read_exact(&mut signature).await?;
+
+ // Read key identifier from target to identify which public key to use
+ let mut key_id_len_buf = [0u8; 4];
+ self.stream.read_exact(&mut key_id_len_buf).await?;
+ let key_id_len = u32::from_be_bytes(key_id_len_buf) as usize;
+
+ let mut key_id_buf = vec![0u8; key_id_len];
+ self.stream.read_exact(&mut key_id_buf).await?;
+ let key_id = String::from_utf8(key_id_buf)
+ .map_err(|e| TcpTargetError::Crypto(format!("Invalid key identifier: {}", e)))?;
+
+ // Load appropriate public key
+ let public_key_path = public_key_dir.as_ref().join(format!("{}.pem", key_id));
+ if !public_key_path.exists() {
+ return Ok((false, key_id));
+ }
+
+ let public_key_pem = tokio::fs::read_to_string(&public_key_path).await?;
+
+ // Try to verify with different key types
+ let verified = if let Ok(rsa_key) = RsaPublicKey::from_pkcs1_pem(&public_key_pem) {
+ let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>();
+ rsa_key.verify(padding, &challenge, &signature).is_ok()
+ } else if let Ok(ed25519_key) =
+ VerifyingKey::from_bytes(&parse_ed25519_public_key(&public_key_pem))
+ {
+ if signature.len() == 64 {
+ let sig_bytes: [u8; 64] = signature.as_slice().try_into().map_err(|_| {
+ TcpTargetError::Crypto("Invalid signature length for Ed25519".to_string())
+ })?;
+ let sig = Signature::from_bytes(&sig_bytes);
+ ed25519_key.verify(&challenge, &sig).is_ok()
+ } else {
+ false
+ }
+ } else if let Ok(dsa_key_info) = parse_dsa_public_key(&public_key_pem) {
+ verify_dsa_signature(&dsa_key_info, &challenge, &signature)
+ } else {
+ false
+ };
+
+ Ok((verified, key_id))
+ }
+
+ /// Accepts a challenge from the target machine to verify connection security
+ ///
+ /// This method performs a cryptographic challenge-response authentication:
+ /// 1. Receives a random 32-byte challenge from the target machine
+ /// 2. Signs the challenge using the appropriate private key
+ /// 3. Sends the digital signature back to the target machine
+ /// 4. Sends the key identifier for public key verification
+ ///
+ /// # Arguments
+ /// * `private_key_file` - Path to the private key file for signing
+ /// * `verify_public_key` - Key identifier for public key verification
+ ///
+ /// # Returns
+ /// * `Ok(true)` - Challenge response sent successfully
+ /// * `Ok(false)` - Private key format not supported
+ /// * `Err(TcpTargetError)` - Error during challenge response process
+ pub async fn accept_challenge(
+ &mut self,
+ private_key_file: impl AsRef<Path>,
+ verify_public_key: &str,
+ ) -> Result<bool, TcpTargetError> {
+ // Read challenge from initiator
+ let mut challenge = [0u8; 32];
+ self.stream.read_exact(&mut challenge).await?;
+
+ // Load private key
+ let private_key_pem = tokio::fs::read_to_string(&private_key_file)
+ .await
+ .map_err(|e| {
+ TcpTargetError::NotFound(format!(
+ "Read private key \"{}\" failed: \"{}\"",
+ private_key_file
+ .as_ref()
+ .display()
+ .to_string()
+ .split("/")
+ .last()
+ .unwrap_or("UNKNOWN"),
+ e
+ ))
+ })?;
+
+ // Sign the challenge with supported key types
+ let signature = if let Ok(rsa_key) = RsaPrivateKey::from_pkcs1_pem(&private_key_pem) {
+ let padding = rsa::pkcs1v15::Pkcs1v15Sign::new::<sha2::Sha256>();
+ rsa_key.sign(padding, &challenge)?
+ } else if let Ok(ed25519_key) = parse_ed25519_private_key(&private_key_pem) {
+ ed25519_key.sign(&challenge).to_bytes().to_vec()
+ } else if let Ok(dsa_key_info) = parse_dsa_private_key(&private_key_pem) {
+ sign_with_dsa(&dsa_key_info, &challenge)?
+ } else {
+ return Ok(false);
+ };
+
+ // Send signature length and signature
+ let signature_len = signature.len() as u32;
+ self.stream.write_all(&signature_len.to_be_bytes()).await?;
+ self.stream.flush().await?;
+ self.stream.write_all(&signature).await?;
+ self.stream.flush().await?;
+
+ // Send key identifier for public key identification
+ let key_id_bytes = verify_public_key.as_bytes();
+ let key_id_len = key_id_bytes.len() as u32;
+ self.stream.write_all(&key_id_len.to_be_bytes()).await?;
+ self.stream.flush().await?;
+ self.stream.write_all(key_id_bytes).await?;
+ self.stream.flush().await?;
+
+ Ok(true)
+ }
+}
+
+/// Parse Ed25519 public key from PEM format
+fn parse_ed25519_public_key(pem: &str) -> [u8; 32] {
+ // Robust parsing for Ed25519 public key using pem crate
+ let mut key_bytes = [0u8; 32];
+
+ if let Ok(pem_data) = pem::parse(pem)
+ && pem_data.tag() == "PUBLIC KEY"
+ && pem_data.contents().len() >= 32
+ {
+ let contents = pem_data.contents();
+ key_bytes.copy_from_slice(&contents[contents.len() - 32..]);
+ }
+ key_bytes
+}
+
+/// Parse Ed25519 private key from PEM format
+fn parse_ed25519_private_key(pem: &str) -> Result<SigningKey, TcpTargetError> {
+ if let Ok(pem_data) = pem::parse(pem)
+ && pem_data.tag() == "PRIVATE KEY"
+ && pem_data.contents().len() >= 32
+ {
+ let contents = pem_data.contents();
+ let mut seed = [0u8; 32];
+ seed.copy_from_slice(&contents[contents.len() - 32..]);
+ return Ok(SigningKey::from_bytes(&seed));
+ }
+ Err(TcpTargetError::Crypto(
+ "Invalid Ed25519 private key format".to_string(),
+ ))
+}
+
+/// Parse DSA public key information from PEM
+fn parse_dsa_public_key(
+ pem: &str,
+) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> {
+ if let Ok(pem_data) = pem::parse(pem) {
+ let contents = pem_data.contents().to_vec();
+
+ // Try different DSA algorithms based on PEM tag
+ match pem_data.tag() {
+ "EC PUBLIC KEY" | "PUBLIC KEY" if pem.contains("ECDSA") || pem.contains("ecdsa") => {
+ if pem.contains("P-256") {
+ return Ok((&ECDSA_P256_SHA256_ASN1, contents));
+ } else if pem.contains("P-384") {
+ return Ok((&ECDSA_P384_SHA384_ASN1, contents));
+ }
+ }
+ "RSA PUBLIC KEY" | "PUBLIC KEY" => {
+ return Ok((&RSA_PKCS1_2048_8192_SHA256, contents));
+ }
+ _ => {}
+ }
+
+ // Default to RSA for unknown types
+ return Ok((&RSA_PKCS1_2048_8192_SHA256, contents));
+ }
+ Err(TcpTargetError::Crypto(
+ "Invalid DSA public key format".to_string(),
+ ))
+}
+
+/// Parse DSA private key information from PEM
+fn parse_dsa_private_key(
+ pem: &str,
+) -> Result<(&'static dyn signature::VerificationAlgorithm, Vec<u8>), TcpTargetError> {
+ // For DSA, private key verification uses the same algorithm as public key
+ parse_dsa_public_key(pem)
+}
+
+/// Verify DSA signature
+fn verify_dsa_signature(
+ algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>),
+ message: &[u8],
+ signature: &[u8],
+) -> bool {
+ let (algorithm, key_bytes) = algorithm_and_key;
+ let public_key = UnparsedPublicKey::new(*algorithm, key_bytes);
+ public_key.verify(message, signature).is_ok()
+}
+
+/// Sign with DSA
+fn sign_with_dsa(
+ algorithm_and_key: &(&'static dyn signature::VerificationAlgorithm, Vec<u8>),
+ message: &[u8],
+) -> Result<Vec<u8>, TcpTargetError> {
+ let (algorithm, key_bytes) = algorithm_and_key;
+
+ // Handle different DSA/ECDSA algorithms by comparing algorithm identifiers
+ // Since we can't directly compare trait objects, we use pointer comparison
+ let algorithm_ptr = algorithm as *const _ as *const ();
+ let ecdsa_p256_ptr = &ECDSA_P256_SHA256_ASN1 as *const _ as *const ();
+ let ecdsa_p384_ptr = &ECDSA_P384_SHA384_ASN1 as *const _ as *const ();
+
+ if algorithm_ptr == ecdsa_p256_ptr {
+ let key_pair = EcdsaKeyPair::from_pkcs8(
+ ECDSA_P256_SHA256_ASN1_SIGNING,
+ key_bytes,
+ &SystemRandom::new(),
+ )
+ .map_err(|e| {
+ TcpTargetError::Crypto(format!("Failed to create ECDSA P-256 key pair: {}", e))
+ })?;
+
+ let signature = key_pair
+ .sign(&SystemRandom::new(), message)
+ .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-256 signing failed: {}", e)))?;
+
+ Ok(signature.as_ref().to_vec())
+ } else if algorithm_ptr == ecdsa_p384_ptr {
+ let key_pair = EcdsaKeyPair::from_pkcs8(
+ ECDSA_P384_SHA384_ASN1_SIGNING,
+ key_bytes,
+ &SystemRandom::new(),
+ )
+ .map_err(|e| {
+ TcpTargetError::Crypto(format!("Failed to create ECDSA P-384 key pair: {}", e))
+ })?;
+
+ let signature = key_pair
+ .sign(&SystemRandom::new(), message)
+ .map_err(|e| TcpTargetError::Crypto(format!("ECDSA P-384 signing failed: {}", e)))?;
+
+ Ok(signature.as_ref().to_vec())
+ } else {
+ // RSA or unsupported algorithm
+ Err(TcpTargetError::Unsupported(
+ "DSA/ECDSA signing not supported for this algorithm type".to_string(),
+ ))
+ }
+}
diff --git a/utils/tcp_connection/src/lib.rs b/utils/tcp_connection/src/lib.rs
new file mode 100644
index 0000000..6a2e599
--- /dev/null
+++ b/utils/tcp_connection/src/lib.rs
@@ -0,0 +1,6 @@
+#[allow(dead_code)]
+pub mod instance;
+
+pub mod instance_challenge;
+
+pub mod error;
diff --git a/utils/tcp_connection/tcp_connection_test/Cargo.toml b/utils/tcp_connection/tcp_connection_test/Cargo.toml
new file mode 100644
index 0000000..19a6e9b
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "tcp_connection_test"
+edition = "2024"
+version.workspace = true
+
+[dependencies]
+tcp_connection = { path = "../../tcp_connection" }
+tokio = { version = "1.48.0", features = ["full"] }
+serde = { version = "1.0.228", features = ["derive"] }
diff --git a/utils/tcp_connection/tcp_connection_test/res/image/test_transfer.png b/utils/tcp_connection/tcp_connection_test/res/image/test_transfer.png
new file mode 100644
index 0000000..5fa94f0
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/res/image/test_transfer.png
Binary files differ
diff --git a/utils/tcp_connection/tcp_connection_test/res/key/test_key.pem b/utils/tcp_connection/tcp_connection_test/res/key/test_key.pem
new file mode 100644
index 0000000..e155876
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/res/key/test_key.pem
@@ -0,0 +1,13 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIICCgKCAgEAl5vyIwGYiQ1zZpW2tg+LwOUV547T2SjlzKQjcms5je/epP4CnUfT
+5cmHCe8ZaSbnofcntCzi8FzMpQmzhNzFk5tCAe4tSrghfr2kYDO7aUL0G09KbNZ5
+iuMTkMaHx6LMjZ+Ljy8fC47yC2dFMUgLjGS7xS6rnIo4YtFuvMdwbLjs7mSn+vVc
+kcEV8RLlQg8wDbzpl66Jd1kiUgPfVLBRTLE/iL8kUCz1l8c+DvOzr3ATwJysM9CG
+LFahGLlTd3CZaj0QsEzf/AQsn79Su+rnCXhXqcvynhAcil0UW9RWp5Zsvp3Me3W8
+pJg6vZuAA6lQ062hkRLiJ91F2rpyqtkax5i/simLjelpsRzLKo6Xsz1bZht2+5d5
+ArgTBtZBxS044t8caZWLXetnPEcxEGz8KYUVKf7X9S7R53gy36y88Fbu9giqUr3m
+b3Da+SYzBT//hacGn55nhzLRdsJGaFFWcKCbpue6JHLsFhizhdEAjaec0hfphw29
+veY0adPdIFLQDmMKaNk4ulrz8Lbgpqn9gxx6fRssj9jqNJmW64a0eV+Rw7BCJazH
+xp3zz4A3rwdI8BjxLUb3YiCUcavA9WzJ1DUfdX1FSvbcFw4CEiGJjfpWGrm1jtc6
+DMOsoX/C6yFOyRpipsgqIToBClchLSNgrO6A7SIoSdIqNDEgIanFcjECAwEAAQ==
+-----END RSA PUBLIC KEY-----
diff --git a/utils/tcp_connection/tcp_connection_test/res/key/test_key_private.pem b/utils/tcp_connection/tcp_connection_test/res/key/test_key_private.pem
new file mode 100644
index 0000000..183d2d9
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/res/key/test_key_private.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/utils/tcp_connection/tcp_connection_test/res/key/wrong_key_private.pem b/utils/tcp_connection/tcp_connection_test/res/key/wrong_key_private.pem
new file mode 100644
index 0000000..4b77eea
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/res/key/wrong_key_private.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+f9qBWGCwdZjeDjaVVUgwi+Waf+M0tQ==
+-----END PRIVATE KEY-----
diff --git a/utils/tcp_connection/tcp_connection_test/src/lib.rs b/utils/tcp_connection/tcp_connection_test/src/lib.rs
new file mode 100644
index 0000000..c9372d4
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/lib.rs
@@ -0,0 +1,17 @@
+#[cfg(test)]
+pub mod test_tcp_target_build;
+
+#[cfg(test)]
+pub mod test_connection;
+
+#[cfg(test)]
+pub mod test_challenge;
+
+#[cfg(test)]
+pub mod test_file_transfer;
+
+#[cfg(test)]
+pub mod test_msgpack;
+
+pub mod test_utils;
+pub use test_utils::*;
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_challenge.rs b/utils/tcp_connection/tcp_connection_test/src/test_challenge.rs
new file mode 100644
index 0000000..9327b3e
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_challenge.rs
@@ -0,0 +1,160 @@
+use std::{env::current_dir, time::Duration};
+
+use tcp_connection::instance::ConnectionInstance;
+use tokio::{
+ join,
+ time::{sleep, timeout},
+};
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target::TcpServerTarget,
+ target_configure::ServerTargetConfig,
+};
+
+pub(crate) struct ExampleChallengeClientHandle;
+
+impl ClientHandle<ExampleChallengeServerHandle> for ExampleChallengeClientHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Accept challenge with correct key
+ let key = current_dir()
+ .unwrap()
+ .join("res")
+ .join("key")
+ .join("test_key_private.pem");
+ let result = instance.accept_challenge(key, "test_key").await.unwrap();
+
+ // Sent success
+ assert!(result);
+ let response = instance.read_text().await.unwrap();
+
+ // Verify success
+ assert_eq!("OK", response);
+
+ // Accept challenge with wrong key
+ let key = current_dir()
+ .unwrap()
+ .join("res")
+ .join("key")
+ .join("wrong_key_private.pem");
+ let result = instance.accept_challenge(key, "test_key").await.unwrap();
+
+ // Sent success
+ assert!(result);
+ let response = instance.read_text().await.unwrap();
+
+ // Verify fail
+ assert_eq!("ERROR", response);
+
+ // Accept challenge with wrong name
+ let key = current_dir()
+ .unwrap()
+ .join("res")
+ .join("key")
+ .join("test_key_private.pem");
+ let result = instance.accept_challenge(key, "test_key__").await.unwrap();
+
+ // Sent success
+ assert!(result);
+ let response = instance.read_text().await.unwrap();
+
+ // Verify fail
+ assert_eq!("ERROR", response);
+ }
+}
+
+pub(crate) struct ExampleChallengeServerHandle;
+
+impl ServerHandle<ExampleChallengeClientHandle> for ExampleChallengeServerHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Challenge with correct key
+ let key_dir = current_dir().unwrap().join("res").join("key");
+ let (result, key_id) = instance.challenge(key_dir).await.unwrap();
+ assert!(result);
+ assert_eq!(key_id, "test_key");
+
+ // Send response
+ instance
+ .write_text(if result { "OK" } else { "ERROR" })
+ .await
+ .unwrap();
+
+ // Challenge again
+ let key_dir = current_dir().unwrap().join("res").join("key");
+ let (result, key_id) = instance.challenge(key_dir).await.unwrap();
+ assert!(!result);
+ assert_eq!(key_id, "test_key");
+
+ // Send response
+ instance
+ .write_text(if result { "OK" } else { "ERROR" })
+ .await
+ .unwrap();
+
+ // Challenge again
+ let key_dir = current_dir().unwrap().join("res").join("key");
+ let (result, key_id) = instance.challenge(key_dir).await.unwrap();
+ assert!(!result);
+ assert_eq!(key_id, "test_key__");
+
+ // Send response
+ instance
+ .write_text(if result { "OK" } else { "ERROR" })
+ .await
+ .unwrap();
+ }
+}
+
+#[tokio::test]
+async fn test_connection_with_challenge_handle() -> Result<(), std::io::Error> {
+ let host = "localhost:5011";
+
+ // Server setup
+ let Ok(server_target) = TcpServerTarget::<
+ ExampleChallengeClientHandle,
+ ExampleChallengeServerHandle,
+ >::from_domain(host)
+ .await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ // Client setup
+ let Ok(client_target) = TcpServerTarget::<
+ ExampleChallengeClientHandle,
+ ExampleChallengeServerHandle,
+ >::from_domain(host)
+ .await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ let future_server = async move {
+ // Only process once
+ let configured_server = server_target.server_cfg(ServerTargetConfig::default().once());
+
+ // Listen here
+ let _ = configured_server.listen().await;
+ };
+
+ let future_client = async move {
+ // Wait for server start
+ let _ = sleep(Duration::from_secs_f32(1.5)).await;
+
+ // Connect here
+ let _ = client_target.connect().await;
+ };
+
+ let test_timeout = Duration::from_secs(10);
+
+ timeout(test_timeout, async { join!(future_client, future_server) })
+ .await
+ .map_err(|_| {
+ std::io::Error::new(
+ std::io::ErrorKind::TimedOut,
+ format!("Test timed out after {:?}", test_timeout),
+ )
+ })?;
+
+ Ok(())
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_connection.rs b/utils/tcp_connection/tcp_connection_test/src/test_connection.rs
new file mode 100644
index 0000000..8c3ab01
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_connection.rs
@@ -0,0 +1,78 @@
+use std::time::Duration;
+
+use tcp_connection::instance::ConnectionInstance;
+use tokio::{join, time::sleep};
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target::TcpServerTarget,
+ target_configure::ServerTargetConfig,
+};
+
+pub(crate) struct ExampleClientHandle;
+
+impl ClientHandle<ExampleServerHandle> for ExampleClientHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Write name
+ let Ok(_) = instance.write_text("Peter").await else {
+ panic!("Write text failed!");
+ };
+ // Read msg
+ let Ok(result) = instance.read_text().await else {
+ return;
+ };
+ assert_eq!("Hello Peter!", result);
+ }
+}
+
+pub(crate) struct ExampleServerHandle;
+
+impl ServerHandle<ExampleClientHandle> for ExampleServerHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Read name
+ let Ok(name) = instance.read_text().await else {
+ return;
+ };
+ // Write msg
+ let Ok(_) = instance.write_text(format!("Hello {}!", name)).await else {
+ panic!("Write text failed!");
+ };
+ }
+}
+
+#[tokio::test]
+async fn test_connection_with_example_handle() {
+ let host = "localhost:5012";
+
+ // Server setup
+ let Ok(server_target) =
+ TcpServerTarget::<ExampleClientHandle, ExampleServerHandle>::from_domain(host).await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ // Client setup
+ let Ok(client_target) =
+ TcpServerTarget::<ExampleClientHandle, ExampleServerHandle>::from_domain(host).await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ let future_server = async move {
+ // Only process once
+ let configured_server = server_target.server_cfg(ServerTargetConfig::default().once());
+
+ // Listen here
+ let _ = configured_server.listen().await;
+ };
+
+ let future_client = async move {
+ // Wait for server start
+ let _ = sleep(Duration::from_secs_f32(1.5)).await;
+
+ // Connect here
+ let _ = client_target.connect().await;
+ };
+
+ let _ = async { join!(future_client, future_server) }.await;
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_file_transfer.rs b/utils/tcp_connection/tcp_connection_test/src/test_file_transfer.rs
new file mode 100644
index 0000000..4237ea7
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_file_transfer.rs
@@ -0,0 +1,94 @@
+use std::{env::current_dir, time::Duration};
+
+use tcp_connection::instance::ConnectionInstance;
+use tokio::{
+ join,
+ time::{sleep, timeout},
+};
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target::TcpServerTarget,
+ target_configure::ServerTargetConfig,
+};
+
+pub(crate) struct ExampleFileTransferClientHandle;
+
+impl ClientHandle<ExampleFileTransferServerHandle> for ExampleFileTransferClientHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ let image_path = current_dir()
+ .unwrap()
+ .join("res")
+ .join("image")
+ .join("test_transfer.png");
+ instance.write_file(image_path).await.unwrap();
+ }
+}
+
+pub(crate) struct ExampleFileTransferServerHandle;
+
+impl ServerHandle<ExampleFileTransferClientHandle> for ExampleFileTransferServerHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ let save_path = current_dir()
+ .unwrap()
+ .join("res")
+ .join(".temp")
+ .join("image")
+ .join("test_transfer.png");
+ instance.read_file(save_path).await.unwrap();
+ }
+}
+
+#[tokio::test]
+async fn test_connection_with_challenge_handle() -> Result<(), std::io::Error> {
+ let host = "localhost:5010";
+
+ // Server setup
+ let Ok(server_target) = TcpServerTarget::<
+ ExampleFileTransferClientHandle,
+ ExampleFileTransferServerHandle,
+ >::from_domain(host)
+ .await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ // Client setup
+ let Ok(client_target) = TcpServerTarget::<
+ ExampleFileTransferClientHandle,
+ ExampleFileTransferServerHandle,
+ >::from_domain(host)
+ .await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ let future_server = async move {
+ // Only process once
+ let configured_server = server_target.server_cfg(ServerTargetConfig::default().once());
+
+ // Listen here
+ let _ = configured_server.listen().await;
+ };
+
+ let future_client = async move {
+ // Wait for server start
+ let _ = sleep(Duration::from_secs_f32(1.5)).await;
+
+ // Connect here
+ let _ = client_target.connect().await;
+ };
+
+ let test_timeout = Duration::from_secs(10);
+
+ timeout(test_timeout, async { join!(future_client, future_server) })
+ .await
+ .map_err(|_| {
+ std::io::Error::new(
+ std::io::ErrorKind::TimedOut,
+ format!("Test timed out after {:?}", test_timeout),
+ )
+ })?;
+
+ Ok(())
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_msgpack.rs b/utils/tcp_connection/tcp_connection_test/src/test_msgpack.rs
new file mode 100644
index 0000000..4c9c870
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_msgpack.rs
@@ -0,0 +1,103 @@
+use serde::{Deserialize, Serialize};
+use std::time::Duration;
+use tcp_connection::instance::ConnectionInstance;
+use tokio::{join, time::sleep};
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target::TcpServerTarget,
+ target_configure::ServerTargetConfig,
+};
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Default)]
+struct TestData {
+ id: u32,
+ name: String,
+}
+
+pub(crate) struct MsgPackClientHandle;
+
+impl ClientHandle<MsgPackServerHandle> for MsgPackClientHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Test basic MessagePack serialization
+ let test_data = TestData {
+ id: 42,
+ name: "Test MessagePack".to_string(),
+ };
+
+ // Write MessagePack data
+ if let Err(e) = instance.write_msgpack(&test_data).await {
+ panic!("Write MessagePack failed: {}", e);
+ }
+
+ // Read response
+ let response: TestData = match instance.read_msgpack().await {
+ Ok(data) => data,
+ Err(e) => panic!("Read MessagePack response failed: {}", e),
+ };
+
+ // Verify response
+ assert_eq!(response.id, test_data.id * 2);
+ assert_eq!(response.name, format!("Processed: {}", test_data.name));
+ }
+}
+
+pub(crate) struct MsgPackServerHandle;
+
+impl ServerHandle<MsgPackClientHandle> for MsgPackServerHandle {
+ async fn process(mut instance: ConnectionInstance) {
+ // Read MessagePack data
+ let received_data: TestData = match instance.read_msgpack().await {
+ Ok(data) => data,
+ Err(_) => return,
+ };
+
+ // Process data
+ let response = TestData {
+ id: received_data.id * 2,
+ name: format!("Processed: {}", received_data.name),
+ };
+
+ // Write response as MessagePack
+ if let Err(e) = instance.write_msgpack(&response).await {
+ panic!("Write MessagePack response failed: {}", e);
+ }
+ }
+}
+
+#[tokio::test]
+async fn test_msgpack_basic() {
+ let host = "localhost:5013";
+
+ // Server setup
+ let Ok(server_target) =
+ TcpServerTarget::<MsgPackClientHandle, MsgPackServerHandle>::from_domain(host).await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ // Client setup
+ let Ok(client_target) =
+ TcpServerTarget::<MsgPackClientHandle, MsgPackServerHandle>::from_domain(host).await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ let future_server = async move {
+ // Only process once
+ let configured_server = server_target.server_cfg(ServerTargetConfig::default().once());
+
+ // Listen here
+ let _ = configured_server.listen().await;
+ };
+
+ let future_client = async move {
+ // Wait for server start
+ let _ = sleep(Duration::from_secs_f32(1.5)).await;
+
+ // Connect here
+ let _ = client_target.connect().await;
+ };
+
+ let _ = async { join!(future_client, future_server) }.await;
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_tcp_target_build.rs b/utils/tcp_connection/tcp_connection_test/src/test_tcp_target_build.rs
new file mode 100644
index 0000000..aa1ec74
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_tcp_target_build.rs
@@ -0,0 +1,32 @@
+use crate::{
+ test_connection::{ExampleClientHandle, ExampleServerHandle},
+ test_utils::target::TcpServerTarget,
+};
+
+#[test]
+fn test_tcp_test_target_build() {
+ let host = "127.0.0.1:8080";
+
+ // Test build target by string
+ let Ok(target) =
+ TcpServerTarget::<ExampleClientHandle, ExampleServerHandle>::from_address_str(host)
+ else {
+ panic!("Test target built failed from a target addr `{}`", host);
+ };
+ assert_eq!(target.to_string(), "127.0.0.1:8080");
+}
+
+#[tokio::test]
+async fn test_tcp_test_target_build_domain() {
+ let host = "localhost";
+
+ // Test build target by DomainName and Connection
+ let Ok(target) =
+ TcpServerTarget::<ExampleClientHandle, ExampleServerHandle>::from_domain(host).await
+ else {
+ panic!("Test target built failed from a domain named `{}`", host);
+ };
+
+ // Test into string
+ assert_eq!(target.to_string(), "127.0.0.1:8080");
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_utils.rs b/utils/tcp_connection/tcp_connection_test/src/test_utils.rs
new file mode 100644
index 0000000..badf27d
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_utils.rs
@@ -0,0 +1,4 @@
+pub mod handle;
+pub mod target;
+pub mod target_configure;
+pub mod target_connection;
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_utils/handle.rs b/utils/tcp_connection/tcp_connection_test/src/test_utils/handle.rs
new file mode 100644
index 0000000..4f9bdbb
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_utils/handle.rs
@@ -0,0 +1,11 @@
+use std::future::Future;
+
+use tcp_connection::instance::ConnectionInstance;
+
+pub trait ClientHandle<RequestServer> {
+ fn process(instance: ConnectionInstance) -> impl Future<Output = ()> + Send;
+}
+
+pub trait ServerHandle<RequestClient> {
+ fn process(instance: ConnectionInstance) -> impl Future<Output = ()> + Send;
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_utils/target.rs b/utils/tcp_connection/tcp_connection_test/src/test_utils/target.rs
new file mode 100644
index 0000000..8972b2a
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_utils/target.rs
@@ -0,0 +1,201 @@
+use serde::{Deserialize, Serialize};
+use std::{
+ fmt::{Display, Formatter},
+ marker::PhantomData,
+ net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr},
+ str::FromStr,
+};
+use tokio::net::lookup_host;
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target_configure::{ClientTargetConfig, ServerTargetConfig},
+};
+
+const DEFAULT_PORT: u16 = 8080;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ /// Client Config
+ client_cfg: Option<ClientTargetConfig>,
+
+ /// Server Config
+ server_cfg: Option<ServerTargetConfig>,
+
+ /// Server port
+ port: u16,
+
+ /// Bind addr
+ bind_addr: IpAddr,
+
+ /// Client Phantom Data
+ _client: PhantomData<Client>,
+
+ /// Server Phantom Data
+ _server: PhantomData<Server>,
+}
+
+impl<Client, Server> Default for TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ fn default() -> Self {
+ Self {
+ client_cfg: None,
+ server_cfg: None,
+ port: DEFAULT_PORT,
+ bind_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
+ _client: PhantomData,
+ _server: PhantomData,
+ }
+ }
+}
+
+impl<Client, Server> From<SocketAddr> for TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ /// Convert SocketAddr to TcpServerTarget
+ fn from(value: SocketAddr) -> Self {
+ Self {
+ port: value.port(),
+ bind_addr: value.ip(),
+ ..Self::default()
+ }
+ }
+}
+
+impl<Client, Server> From<TcpServerTarget<Client, Server>> for SocketAddr
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ /// Convert TcpServerTarget to SocketAddr
+ fn from(val: TcpServerTarget<Client, Server>) -> Self {
+ SocketAddr::new(val.bind_addr, val.port)
+ }
+}
+
+impl<Client, Server> Display for TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}:{}", self.bind_addr, self.port)
+ }
+}
+
+impl<Client, Server> TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ /// Create target by address
+ pub fn from_addr(addr: impl Into<IpAddr>, port: impl Into<u16>) -> Self {
+ Self {
+ port: port.into(),
+ bind_addr: addr.into(),
+ ..Self::default()
+ }
+ }
+
+ /// Try to create target by string
+ pub fn from_address_str<'a>(addr_str: impl Into<&'a str>) -> Result<Self, AddrParseError> {
+ let socket_addr = SocketAddr::from_str(addr_str.into());
+ match socket_addr {
+ Ok(socket_addr) => Ok(Self::from_addr(socket_addr.ip(), socket_addr.port())),
+ Err(err) => Err(err),
+ }
+ }
+
+ /// Try to create target by domain name
+ pub async fn from_domain<'a>(domain: impl Into<&'a str>) -> Result<Self, std::io::Error> {
+ match domain_to_addr(domain).await {
+ Ok(domain_addr) => Ok(Self::from(domain_addr)),
+ Err(e) => Err(e),
+ }
+ }
+
+ /// Set client config
+ pub fn client_cfg(mut self, config: ClientTargetConfig) -> Self {
+ self.client_cfg = Some(config);
+ self
+ }
+
+ /// Set server config
+ pub fn server_cfg(mut self, config: ServerTargetConfig) -> Self {
+ self.server_cfg = Some(config);
+ self
+ }
+
+ /// Add client config
+ pub fn add_client_cfg(&mut self, config: ClientTargetConfig) {
+ self.client_cfg = Some(config);
+ }
+
+ /// Add server config
+ pub fn add_server_cfg(&mut self, config: ServerTargetConfig) {
+ self.server_cfg = Some(config);
+ }
+
+ /// Get client config ref
+ pub fn get_client_cfg(&self) -> Option<&ClientTargetConfig> {
+ self.client_cfg.as_ref()
+ }
+
+ /// Get server config ref
+ pub fn get_server_cfg(&self) -> Option<&ServerTargetConfig> {
+ self.server_cfg.as_ref()
+ }
+
+ /// Get SocketAddr of TcpServerTarget
+ pub fn get_addr(&self) -> SocketAddr {
+ SocketAddr::new(self.bind_addr, self.port)
+ }
+}
+
+/// Parse Domain Name to IpAddr via DNS
+async fn domain_to_addr<'a>(domain: impl Into<&'a str>) -> Result<SocketAddr, std::io::Error> {
+ let domain = domain.into();
+ let default_port: u16 = DEFAULT_PORT;
+
+ if let Ok(socket_addr) = domain.parse::<SocketAddr>() {
+ return Ok(match socket_addr.ip() {
+ IpAddr::V4(_) => socket_addr,
+ IpAddr::V6(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), socket_addr.port()),
+ });
+ }
+
+ if let Ok(_v6_addr) = domain.parse::<std::net::Ipv6Addr>() {
+ return Ok(SocketAddr::new(
+ IpAddr::V4(Ipv4Addr::LOCALHOST),
+ default_port,
+ ));
+ }
+
+ let (host, port_str) = if let Some((host, port)) = domain.rsplit_once(':') {
+ (host.trim_matches(|c| c == '[' || c == ']'), Some(port))
+ } else {
+ (domain, None)
+ };
+
+ let port = port_str
+ .and_then(|p| p.parse::<u16>().ok())
+ .map(|p| p.clamp(0, u16::MAX))
+ .unwrap_or(default_port);
+
+ let mut socket_iter = lookup_host((host, 0)).await?;
+
+ if let Some(addr) = socket_iter.find(|addr| addr.is_ipv4()) {
+ return Ok(SocketAddr::new(addr.ip(), port));
+ }
+
+ Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port))
+}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_utils/target_configure.rs b/utils/tcp_connection/tcp_connection_test/src/test_utils/target_configure.rs
new file mode 100644
index 0000000..d739ac9
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_utils/target_configure.rs
@@ -0,0 +1,53 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
+pub struct ServerTargetConfig {
+ /// Only process a single connection, then shut down the server.
+ once: bool,
+
+ /// Timeout duration in milliseconds. (0 is Closed)
+ timeout: u64,
+}
+
+impl ServerTargetConfig {
+ /// Set `once` to True
+ /// This method configures the `once` field of `ServerTargetConfig`.
+ pub fn once(mut self) -> Self {
+ self.once = true;
+ self
+ }
+
+ /// Set `timeout` to the given value
+ /// This method configures the `timeout` field of `ServerTargetConfig`.
+ pub fn timeout(mut self, timeout: u64) -> Self {
+ self.timeout = timeout;
+ self
+ }
+
+ /// Set `once` to the given value
+ /// This method configures the `once` field of `ServerTargetConfig`.
+ pub fn set_once(&mut self, enable: bool) {
+ self.once = enable;
+ }
+
+ /// Set `timeout` to the given value
+ /// This method configures the `timeout` field of `ServerTargetConfig`.
+ pub fn set_timeout(&mut self, timeout: u64) {
+ self.timeout = timeout;
+ }
+
+ /// Check if the server is configured to process only a single connection.
+ /// Returns `true` if the server will shut down after processing one connection.
+ pub fn is_once(&self) -> bool {
+ self.once
+ }
+
+ /// Get the current timeout value in milliseconds.
+ /// Returns the timeout duration. A value of 0 indicates the connection is closed.
+ pub fn get_timeout(&self) -> u64 {
+ self.timeout
+ }
+}
+
+#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
+pub struct ClientTargetConfig {}
diff --git a/utils/tcp_connection/tcp_connection_test/src/test_utils/target_connection.rs b/utils/tcp_connection/tcp_connection_test/src/test_utils/target_connection.rs
new file mode 100644
index 0000000..d5bf2c3
--- /dev/null
+++ b/utils/tcp_connection/tcp_connection_test/src/test_utils/target_connection.rs
@@ -0,0 +1,89 @@
+use tcp_connection::{error::TcpTargetError, instance::ConnectionInstance};
+use tokio::{
+ net::{TcpListener, TcpSocket},
+ spawn,
+};
+
+use crate::test_utils::{
+ handle::{ClientHandle, ServerHandle},
+ target::TcpServerTarget,
+ target_configure::ServerTargetConfig,
+};
+
+impl<Client, Server> TcpServerTarget<Client, Server>
+where
+ Client: ClientHandle<Server>,
+ Server: ServerHandle<Client>,
+{
+ /// Attempts to establish a connection to the TCP server.
+ ///
+ /// This function initiates a connection to the server address
+ /// specified in the target configuration.
+ ///
+ /// This is a Block operation.
+ pub async fn connect(&self) -> Result<(), TcpTargetError> {
+ let addr = self.get_addr();
+ let Ok(socket) = TcpSocket::new_v4() else {
+ return Err(TcpTargetError::from("Create tcp socket failed!"));
+ };
+ let stream = match socket.connect(addr).await {
+ Ok(stream) => stream,
+ Err(e) => {
+ let err = format!("Connect to `{}` failed: {}", addr, e);
+ return Err(TcpTargetError::from(err));
+ }
+ };
+ let instance = ConnectionInstance::from(stream);
+ Client::process(instance).await;
+ Ok(())
+ }
+
+ /// Attempts to establish a connection to the TCP server.
+ ///
+ /// This function initiates a connection to the server address
+ /// specified in the target configuration.
+ pub async fn listen(&self) -> Result<(), TcpTargetError> {
+ let addr = self.get_addr();
+ let listener = match TcpListener::bind(addr).await {
+ Ok(listener) => listener,
+ Err(_) => {
+ let err = format!("Bind to `{}` failed", addr);
+ return Err(TcpTargetError::from(err));
+ }
+ };
+
+ let cfg: ServerTargetConfig = match self.get_server_cfg() {
+ Some(cfg) => *cfg,
+ None => ServerTargetConfig::default(),
+ };
+
+ if cfg.is_once() {
+ // Process once (Blocked)
+ let (stream, _) = match listener.accept().await {
+ Ok(result) => result,
+ Err(e) => {
+ let err = format!("Accept connection failed: {}", e);
+ return Err(TcpTargetError::from(err));
+ }
+ };
+ let instance = ConnectionInstance::from(stream);
+ Server::process(instance).await;
+ } else {
+ loop {
+ // Process multiple times (Concurrent)
+ let (stream, _) = match listener.accept().await {
+ Ok(result) => result,
+ Err(e) => {
+ let err = format!("Accept connection failed: {}", e);
+ return Err(TcpTargetError::from(err));
+ }
+ };
+ let instance = ConnectionInstance::from(stream);
+ spawn(async move {
+ Server::process(instance).await;
+ });
+ }
+ }
+ Ok(())
+ }
+}