docs(project): added docstrings to all code files

This commit is contained in:
Patryk Hegenberg 2024-05-29 23:55:47 +02:00
parent 47acdec22a
commit 0d354efb8e
24 changed files with 1826 additions and 107 deletions

View file

@ -7,10 +7,22 @@ use tracing::debug;
use crate::config::GLOBAL_CONFIG; use crate::config::GLOBAL_CONFIG;
/// Struct representing the command line arguments parsed by clap.
///
/// It uses the clap library to define the command line arguments and their
/// attributes. The version of the application is obtained from the cargo.toml
/// file.
///
/// The `command` field is an optional subcommand. It is represented by the
/// `Commands` enum which defines the different subcommands that can be used.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version = env!("CARGO_PKG_VERSION"), about = "Send and receive files securely")] #[command(version = env!("CARGO_PKG_VERSION"), about = "Send and receive files securely")]
#[command(long_about = None)] #[command(long_about = None)]
pub struct Args { pub struct Args {
/// The subcommand to run.
///
/// It is an optional field. If it is not provided, the program will run without
/// any specific subcommand.
#[command(subcommand)] #[command(subcommand)]
pub command: Option<Commands>, pub command: Option<Commands>,
} }
@ -32,10 +44,6 @@ pub enum Commands {
#[arg(short, long)] #[arg(short, long)]
relay: Option<String>, relay: Option<String>,
/// Overwrite existing Files
#[arg(short, long)]
overwrite: bool,
/// Name of Transfer to download files /// Name of Transfer to download files
#[arg(value_name = "Transfer_Name")] #[arg(value_name = "Transfer_Name")]
name: String, name: String,
@ -51,34 +59,69 @@ pub enum Commands {
}, },
} }
/// Default implementation of the `Args` struct.
///
/// This implementation uses the `new` method to create a new instance of `Args`.
impl Default for Args { impl Default for Args {
/// Creates a new instance of `Args` by calling the `new` method.
///
/// # Returns
///
/// A new instance of `Args`.
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
/// Struct representing the parsed command line arguments.
///
/// This struct implements the `Default` trait to create a new instance of `Args` by calling the
/// `new` method.
///
/// The `run` method is used to execute the corresponding command based on the parsed arguments.
impl Args { impl Args {
/// Creates a new instance of `Args` by calling the `parse` method.
pub fn new() -> Self { pub fn new() -> Self {
Self::parse() Self::parse()
} }
/// Executes the corresponding command based on the parsed arguments.
///
/// This method takes no parameters.
///
/// # Returns
///
/// A `Result` that either returns `Ok(())` indicating successful execution or an `Err`
/// indicating an error.
pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Retrieve the global configuration
let cfg = &GLOBAL_CONFIG; let cfg = &GLOBAL_CONFIG;
debug!("args: {:#?}", self); debug!("args: {:#?}", self);
// Match on the `command` field of `Args` to execute the corresponding command
match &self.command { match &self.command {
// Command to send files to the receiver or relay server
Some(Commands::Send { relay, files }) => { Some(Commands::Send { relay, files }) => {
// Create a string representation of the relay address
let relay_string: String = relay.as_deref().unwrap_or(&cfg.app_origin).to_string(); let relay_string: String = relay.as_deref().unwrap_or(&cfg.app_origin).to_string();
// Create Arc wrappers for the relay address and file paths
let relay_arc = Arc::new(relay_string); let relay_arc = Arc::new(relay_string);
let files_arc = Arc::new(files.to_vec()); let files_arc = Arc::new(files.to_vec());
// Generate a random name
let rand_name = generate_random_name(); let rand_name = generate_random_name();
// Start the sender with the generated name, relay address, and file paths
sender::start_sender(rand_name, relay_arc, files_arc).await; sender::start_sender(rand_name, relay_arc, files_arc).await;
} }
// Command to receive files from the sender with the matching password
Some(Commands::Receive { Some(Commands::Receive {
relay, relay,
overwrite: _,
name, name,
}) => { }) => {
// Print the received transfer name
println!("Receive for {name:?}"); println!("Receive for {name:?}");
// Start the receiver with the current directory, relay address, and transfer name
let _ = receiver::start_receiver( let _ = receiver::start_receiver(
".".to_string(), ".".to_string(),
relay.as_deref().unwrap_or(&cfg.app_origin), relay.as_deref().unwrap_or(&cfg.app_origin),
@ -86,19 +129,23 @@ impl Args {
) )
.await; .await;
} }
// Command to start a relay server
Some(Commands::Serve { Some(Commands::Serve {
port, port,
listen_address, listen_address,
}) => { }) => {
println!("Serve with address '{listen_address:?}' and '{port:?}'"); // Create a string representation of the listen address
let address: String = listen_address let address: String = listen_address
.as_deref() .as_deref()
.unwrap_or(&cfg.app_host) .unwrap_or(&cfg.app_host)
.to_string(); .to_string();
// Create an integer representation of the port
let port_value = port.unwrap_or(cfg.app_port.parse::<i32>().unwrap_or(0)); let port_value = port.unwrap_or(cfg.app_port.parse::<i32>().unwrap_or(0));
let port: i32 = port_value; let port: i32 = port_value;
// Start the relay server with the port and listen address
relay::server::start_ws(&port, &address).await; relay::server::start_ws(&port, &address).await;
} }
// No command provided
None => {} None => {}
} }
Ok(()) Ok(())

View file

@ -1,25 +1,70 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Represents the configuration settings for the Caesar application.
///
/// This struct is used to store the configuration settings for the application,
/// such as the environment, host, port, origin, and logging level.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CaesarConfig { pub struct CaesarConfig {
/// The environment in which the application is running.
///
/// Possible values are "production", "staging", or "development".
pub app_environment: String, pub app_environment: String,
/// The host on which the application is running.
///
/// This is typically an IP address or a hostname.
pub app_host: String, pub app_host: String,
/// The port on which the application is listening.
///
/// This is typically a string representation of a port number.
pub app_port: String, pub app_port: String,
/// The origin of the application.
///
/// This is typically a URL that specifies the protocol, hostname, and port.
pub app_origin: String, pub app_origin: String,
/// The relay endpoint of the application.
///
/// This is typically a combination of a hostname and port.
pub app_relay: String, pub app_relay: String,
/// The logging level for the application.
///
/// This is typically a string representation of a logging level, such as "info",
/// "debug", or "error".
pub rust_log: String, pub rust_log: String,
} }
/// The default configuration values for the Caesar application.
///
/// These values are used when loading the configuration file fails.
/// The default configuration is suitable for running the application in a production environment.
impl Default for CaesarConfig { impl Default for CaesarConfig {
/// Returns a new `CaesarConfig` instance with default values.
///
/// # Returns
///
/// A new `CaesarConfig` instance with the following default values:
///
/// - `app_environment`: "production"
/// - `app_host`: "0.0.0.0"
/// - `app_port`: "8000"
/// - `app_origin`: "wss://caesar-transfer-iu.shuttleapp.rs"
/// - `app_relay`: "0.0.0.0:8000"
/// - `rust_log`: "info"
fn default() -> Self { fn default() -> Self {
CaesarConfig { CaesarConfig {
app_environment: "production".to_string(), app_environment: "production".to_string(), // The environment in which the application is running.
app_host: "0.0.0.0".to_string(), app_host: "0.0.0.0".to_string(), // The host on which the application is running.
app_port: "8000".to_string(), app_port: "8000".to_string(), // The port on which the application is listening.
app_origin: "wss://caesar-transfer-iu.shuttleapp.rs".to_string(), app_origin: "wss://caesar-transfer-iu.shuttleapp.rs".to_string(), // The origin of the application.
app_relay: "0.0.0.0:8000".to_string(), app_relay: "0.0.0.0:8000".to_string(), // The relay endpoint of the application.
rust_log: "info".to_string(), rust_log: "info".to_string(), // The logging level for the application.
} }
} }
} }

View file

@ -6,15 +6,29 @@ use tracing_subscriber::filter::EnvFilter;
mod cli; mod cli;
mod config; mod config;
/// Entry point of the application.
///
/// This function is called when the application is started. It initializes the environment,
/// parses the command line arguments, and runs the application.
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load environment variables from the `.env` file.
dotenv().ok(); dotenv().ok();
// Initialize the logging subscriber.
// It configures the logging level based on the `RUST_LOG` environment variable.
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.init(); .init();
// Parse the command line arguments.
let args = Args::new(); let args = Args::new();
// Run the application.
// If an error occurs, log the error message.
if let Err(e) = args.run().await { if let Err(e) = args.run().await {
error!("{e}"); error!("{e}");
} }
Ok(()) Ok(())
} }

View file

@ -25,62 +25,185 @@ const NONCE_SIZE: usize = 12;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
const FILE_PATH_PREFIX: &str = "/storage/emulated/0/Download"; const FILE_PATH_PREFIX: &str = "/storage/emulated/0/Download";
/// Represents a file to be transferred.
///
/// # Fields
///
/// - `name`: The name of the file.
/// - `size`: The total size of the file in bytes.
/// - `progress`: The number of bytes that have been transferred so far.
/// - `handle`: The file handle for reading and writing the file.
#[derive(Debug)]
struct File { struct File {
/// The name of the file.
name: String, name: String,
/// The total size of the file in bytes.
size: u64, size: u64,
/// The number of bytes that have been transferred so far.
progress: u64, progress: u64,
/// The file handle for reading and writing the file.
handle: fs::File, handle: fs::File,
} }
/// Represents the state of the receiver.
///
/// # Fields
///
/// - `hmac`: The HMAC key used for authentication.
/// - `sender`: The sender used for sending packets.
/// - `key`: The ephemeral secret key used for key agreement.
/// - `shared_key`: The shared key used for encryption.
/// - `files`: The list of files being transferred.
/// - `sequence`: The sequence number of the last received packet.
/// - `index`: The index of the current file being transferred.
/// - `progress`: The number of bytes transferred so far.
/// - `length`: The total length of the file being transferred.
struct Context { struct Context {
/// The HMAC key used for authentication.
hmac: Vec<u8>, hmac: Vec<u8>,
/// The sender used for sending packets.
sender: Sender, sender: Sender,
/// The ephemeral secret key used for key agreement.
key: EphemeralSecret, key: EphemeralSecret,
/// The shared key used for encryption.
shared_key: Option<Aes128Gcm>, shared_key: Option<Aes128Gcm>,
/// The list of files being transferred.
files: Vec<File>, files: Vec<File>,
/// The sequence number of the last received packet.
sequence: u32, sequence: u32,
/// The index of the current file being transferred.
index: usize, index: usize,
/// The number of bytes transferred so far.
progress: u64, progress: u64,
/// The total length of the file being transferred.
length: u64, length: u64,
} }
/// Handle the join room packet.
///
/// # Arguments
///
/// * `size` - The size of the room.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if the join room packet is invalid.
fn on_join_room(size: Option<usize>) -> Status { fn on_join_room(size: Option<usize>) -> Status {
// Check if the size of the room is provided
if size.is_none() { if size.is_none() {
// Return an error if the join room packet is invalid
return Status::Err("Invalid join room packet.".into()); return Status::Err("Invalid join room packet.".into());
} }
// Print a message indicating that the client has successfully connected to the room
println!("Connected to room."); println!("Connected to room.");
// Return a continue status to indicate that the operation was successful
Status::Continue() Status::Continue()
} }
/// Handle the error packet.
///
/// # Arguments
///
/// * `message` - The error message.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error with the provided error message.
fn on_error(message: String) -> Status { fn on_error(message: String) -> Status {
// Return an error with the provided error message
Status::Err(message) Status::Err(message)
} }
/// Handle the leave room packet.
///
/// # Arguments
///
/// * `context` - The receiver context.
/// * `_` - The index of the sender. Currently unused.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if there are still files being transferred.
fn on_leave_room(context: &mut Context, _: usize) -> Status { fn on_leave_room(context: &mut Context, _: usize) -> Status {
// Check if there are any files being transferred with less than 100% progress
if context.files.iter().any(|file| file.progress < 100) { if context.files.iter().any(|file| file.progress < 100) {
// Print a message indicating that the transfer was interrupted because the host left the room
println!(); println!();
println!("Transfer was interrupted because the host left the room."); println!("Transfer was interrupted because the host left the room.");
// Return an error with the provided message
Status::Err("Transfer was interrupted because the host left the room.".into()) Status::Err("Transfer was interrupted because the host left the room.".into())
} else { } else {
// Return an exit status to indicate that the operation was successful
Status::Exit() Status::Exit()
} }
} }
/// Handle the list packet.
///
/// # Arguments
///
/// * `filepath` - The path to the directory where the files will be saved.
/// * `context` - The receiver context.
/// * `list` - The list packet containing the files to be transferred.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if the list packet is invalid or if a file with the same name already exists.
fn on_list(filepath: String, context: &mut Context, list: ListPacket) -> Status { fn on_list(filepath: String, context: &mut Context, list: ListPacket) -> Status {
// Check if the shared key is established
if context.shared_key.is_none() { if context.shared_key.is_none() {
return Status::Err("Invalid list packet: no shared key established".into()); return Status::Err("Invalid list packet: no shared key established".into());
} }
// Iterate over the entries in the list packet
for entry in list.entries { for entry in list.entries {
// Sanitize the filename to prevent directory traversal attacks
let path = sanitize_filename::sanitize(entry.name.clone()); let path = sanitize_filename::sanitize(entry.name.clone());
// Construct the file path
let file_path = format!("{}/{}", filepath, path); let file_path = format!("{}/{}", filepath, path);
// Check if the file already exists
if Path::new(&file_path).exists() { if Path::new(&file_path).exists() {
return Status::Err(format!("The file '{}' already exists.", path)); return Status::Err(format!("The file '{}' already exists.", path));
} }
// Create a new file
let handle = match fs::File::create(&file_path) { let handle = match fs::File::create(&file_path) {
Ok(handle) => handle, Ok(handle) => handle,
Err(error) => { Err(error) => {
@ -91,6 +214,7 @@ fn on_list(filepath: String, context: &mut Context, list: ListPacket) -> Status
} }
}; };
// Create a new file object and add it to the context
let file = File { let file = File {
name: entry.name, name: entry.name,
size: entry.size, size: entry.size,
@ -101,6 +225,7 @@ fn on_list(filepath: String, context: &mut Context, list: ListPacket) -> Status
context.files.push(file); context.files.push(file);
} }
// Reset the context for the next file transfer
context.index = 0; context.index = 0;
context.progress = 0; context.progress = 0;
context.sequence = 0; context.sequence = 0;
@ -109,11 +234,28 @@ fn on_list(filepath: String, context: &mut Context, list: ListPacket) -> Status
Status::Continue() Status::Continue()
} }
/// Handle a chunk packet.
///
/// This function is responsible for processing chunk packets received from the sender.
/// It checks if the shared key has been established, verifies the sequence number,
/// writes the chunk to the corresponding file, updates the file's progress, sends progress
/// updates if necessary, and handles the end of a file transfer.
///
/// # Arguments
///
/// * `context` - The receiver context.
/// * `chunk` - The chunk packet received from the sender.
///
/// # Returns
///
/// A status indicating if the operation was successful.
fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status { fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
// Check if the shared key is established
if context.shared_key.is_none() { if context.shared_key.is_none() {
return Status::Err("Invalid chunk packet: no shared key established".into()); return Status::Err("Invalid chunk packet: no shared key established".into());
} }
// Verify the sequence number
if chunk.sequence != context.sequence { if chunk.sequence != context.sequence {
return Status::Err(format!( return Status::Err(format!(
"Expected sequence {}, but got {}.", "Expected sequence {}, but got {}.",
@ -121,18 +263,24 @@ fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
)); ));
} }
// Get the file corresponding to the current index
let Some(file) = context.files.get_mut(context.index) else { let Some(file) = context.files.get_mut(context.index) else {
return Status::Err("Invalid file index.".into()); return Status::Err("Invalid file index.".into());
}; };
// Update the file's length
context.length += chunk.chunk.len() as u64; context.length += chunk.chunk.len() as u64;
// Increment the sequence number
context.sequence += 1; context.sequence += 1;
// Write the chunk to the file
file.handle.write(&chunk.chunk).unwrap(); file.handle.write(&chunk.chunk).unwrap();
// Update the file's progress
file.progress = (context.length * 100) / file.size; file.progress = (context.length * 100) / file.size;
// Send progress updates if necessary
if file.progress == 100 || file.progress - context.progress >= 1 || chunk.sequence == 0 { if file.progress == 100 || file.progress - context.progress >= 1 || chunk.sequence == 0 {
context.progress = file.progress; context.progress = file.progress;
@ -151,6 +299,7 @@ fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
std::io::Write::flush(&mut stdout()).unwrap(); std::io::Write::flush(&mut stdout()).unwrap();
} }
// Handle the end of a file transfer
if file.size == context.length { if file.size == context.length {
context.index += 1; context.index += 1;
context.length = 0; context.length = 0;
@ -163,59 +312,104 @@ fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
Status::Continue() Status::Continue()
} }
/// Handle the handshake packet.
///
/// This function is responsible for handling the handshake packet received from the sender.
/// It performs the necessary verification and establishes the shared key between the sender and receiver.
///
/// # Arguments
///
/// * `context` - The receiver context.
/// * `handshake` - The handshake packet received from the sender.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
fn on_handshake(context: &mut Context, handshake: HandshakePacket) -> Status { fn on_handshake(context: &mut Context, handshake: HandshakePacket) -> Status {
// Check if the shared key is already established
if context.shared_key.is_some() { if context.shared_key.is_some() {
return Status::Err("Already performed handshake.".into()); return Status::Err("Already performed handshake.".into());
} }
// Create a HMAC instance using the shared key
let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap(); let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap();
// Update the HMAC with the sender's public key
mac.update(&handshake.public_key); mac.update(&handshake.public_key);
// Verify the signature using the HMAC
let verification = mac.verify_slice(&handshake.signature); let verification = mac.verify_slice(&handshake.signature);
if verification.is_err() { if verification.is_err() {
return Status::Err("Invalid signature from the sender.".into()); return Status::Err("Invalid signature from the sender.".into());
} }
// Generate the receiver's public key
let public_key = context.key.public_key().to_sec1_bytes().into_vec(); let public_key = context.key.public_key().to_sec1_bytes().into_vec();
// Create a new HMAC instance using the shared key
let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap(); let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap();
// Update the HMAC with the receiver's public key
mac.update(&public_key); mac.update(&public_key);
// Generate the signature using the HMAC
let signature = mac.finalize().into_bytes().to_vec(); let signature = mac.finalize().into_bytes().to_vec();
// Convert the sender's public key into a `PublicKey` object
let shared_public_key = PublicKey::from_sec1_bytes(&handshake.public_key).unwrap(); let shared_public_key = PublicKey::from_sec1_bytes(&handshake.public_key).unwrap();
// Perform Diffie-Hellman key exchange
let shared_secret = context.key.diffie_hellman(&shared_public_key); let shared_secret = context.key.diffie_hellman(&shared_public_key);
let shared_secret = shared_secret.raw_secret_bytes(); let shared_secret = shared_secret.raw_secret_bytes();
let shared_secret = &shared_secret[0..16]; let shared_secret = &shared_secret[0..16];
// Create a new 128-bit AES-GCM key from the shared secret
let shared_key: &Key<Aes128Gcm> = shared_secret.into(); let shared_key: &Key<Aes128Gcm> = shared_secret.into();
let shared_key = <Aes128Gcm as aes_gcm::KeyInit>::new(shared_key); let shared_key = <Aes128Gcm as aes_gcm::KeyInit>::new(shared_key);
// Create the handshake response packet
let handshake_response = HandshakeResponsePacket { let handshake_response = HandshakeResponsePacket {
public_key, public_key,
signature, signature,
}; };
// Send the handshake response packet to the sender
context context
.sender .sender
.send_packet(DESTINATION, Value::HandshakeResponse(handshake_response)); .send_packet(DESTINATION, Value::HandshakeResponse(handshake_response));
// Establish the shared key
context.shared_key = Some(shared_key); context.shared_key = Some(shared_key);
Status::Continue() Status::Continue()
} }
/// Handle a message received from the WebSocket connection.
///
/// This function takes a `filepath` string, a mutable reference to a `Context` struct,
/// and a `WebSocketMessage` enum. It returns a `Status` enum.
///
/// # Arguments
///
/// * `filepath` - A string representing the file path.
/// * `context` - A mutable reference to a `Context` struct.
/// * `message` - A `WebSocketMessage` enum.
///
/// # Returns
///
/// A `Status` enum.
fn on_message(filepath: String, context: &mut Context, message: WebSocketMessage) -> Status { fn on_message(filepath: String, context: &mut Context, message: WebSocketMessage) -> Status {
// Handle text messages
match message.clone() { match message.clone() {
WebSocketMessage::Text(text) => { WebSocketMessage::Text(text) => {
// Parse the JSON packet
let packet = match serde_json::from_str(&text) { let packet = match serde_json::from_str(&text) {
Ok(packet) => packet, Ok(packet) => packet,
Err(_) => { Err(_) => {
return Status::Continue(); return Status::Continue();
} }
}; };
// Handle different types of JSON packets
return match packet { return match packet {
JsonPacketResponse::Join { size } => on_join_room(size), JsonPacketResponse::Join { size } => on_join_room(size),
JsonPacketResponse::Leave { index } => on_leave_room(context, index), JsonPacketResponse::Leave { index } => on_leave_room(context, index),
@ -223,7 +417,9 @@ fn on_message(filepath: String, context: &mut Context, message: WebSocketMessage
_ => Status::Err(format!("Unexpected json packet: {:?}", packet)), _ => Status::Err(format!("Unexpected json packet: {:?}", packet)),
}; };
} }
// Handle binary messages
WebSocketMessage::Binary(data) => { WebSocketMessage::Binary(data) => {
// Extract the data from the binary message
let data = &data[1..]; let data = &data[1..];
let data = if let Some(shared_key) = &context.shared_key { let data = if let Some(shared_key) = &context.shared_key {
@ -235,8 +431,10 @@ fn on_message(filepath: String, context: &mut Context, message: WebSocketMessage
data.to_vec() data.to_vec()
}; };
// Decode the packet
let packet = Packet::decode(data.as_ref()).unwrap(); let packet = Packet::decode(data.as_ref()).unwrap();
let value = packet.value.unwrap(); let value = packet.value.unwrap();
// Handle different types of packets
return match value { return match value {
Value::List(list) => on_list(filepath, context, list), Value::List(list) => on_list(filepath, context, list),
Value::Chunk(chunk) => on_chunk(context, chunk), Value::Chunk(chunk) => on_chunk(context, chunk),
@ -247,9 +445,26 @@ fn on_message(filepath: String, context: &mut Context, message: WebSocketMessage
_ => (), _ => (),
} }
// Return an error status for invalid message types
Status::Err("Invalid message type".into()) Status::Err("Invalid message type".into())
} }
/// Starts the receiver's client.
///
/// This function takes in a file path, a socket, and a fragment string. It
/// then extracts the room ID and HMAC from the fragment string. The function
/// also generates an ephemeral secret key.
///
/// The function initializes a `Context` struct with the extracted information
/// and sets up the necessary communication channels. It then sends a join
/// request to the server and starts handling incoming messages.
///
/// # Arguments
///
/// * `filepath` - The path to the file to be received.
/// * `socket` - The WebSocket connection to the server.
/// * `fragment` - The invite code containing the room ID and HMAC.
pub async fn start(filepath: String, socket: Socket, fragment: &str) { pub async fn start(filepath: String, socket: Socket, fragment: &str) {
let Some(index) = fragment.rfind('-') else { let Some(index) = fragment.rfind('-') else {
println!("Error: The invite code '{}' is not valid.", fragment); println!("Error: The invite code '{}' is not valid.", fragment);

View file

@ -5,31 +5,65 @@ use sha2::{Digest, Sha256};
use crate::relay::transfer::TransferResponse; use crate::relay::transfer::TransferResponse;
/// Fetches download information from the relay server for the given file name.
///
/// # Arguments
///
/// * `relay` - The URL of the relay server.
/// * `name` - The name of the file.
///
/// # Returns
///
/// A future that resolves to a `Result` containing the download information
/// if the request is successful, or an error if the request fails.
pub async fn download_info(relay: &str, name: &str) -> Result<TransferResponse> { pub async fn download_info(relay: &str, name: &str) -> Result<TransferResponse> {
// Convert the relay server URL and file name to strings
let url = String::from(relay); let url = String::from(relay);
let hashed_name = Sha256::digest(name.as_bytes()); let hashed_name = Sha256::digest(name.as_bytes());
let hashed_string = hex::encode(hashed_name); let hashed_string = hex::encode(hashed_name);
// Send a GET request to the relay server with the file name hash as a query parameter
let resp = reqwest::get(format!("{}/download/{}", url, hashed_string)) let resp = reqwest::get(format!("{}/download/{}", url, hashed_string))
.await .await
// If the request fails, return an error with the reason
.map_err(|e| anyhow!("Failed to send GET request: {}", e))?; .map_err(|e| anyhow!("Failed to send GET request: {}", e))?;
// Parse the response body as JSON into a `TransferResponse` struct
resp.json::<TransferResponse>() resp.json::<TransferResponse>()
.await .await
// If the JSON parsing fails, return an error with the reason
.map_err(|e| anyhow!("Failed to parse JSON response: {}", e)) .map_err(|e| anyhow!("Failed to parse JSON response: {}", e))
} }
/// Notifies the relay server that the file download was successful for the given file name.
///
/// # Arguments
///
/// * `relay` - The URL of the relay server.
/// * `name` - The name of the file.
///
/// # Returns
///
/// A future that resolves to a `Result` containing `Ok(())` if the request is successful,
/// or an error if the request fails.
pub async fn download_success(relay: &str, name: &str) -> Result<()> { pub async fn download_success(relay: &str, name: &str) -> Result<()> {
// Convert the relay server URL and file name to strings
let url = String::from(relay); let url = String::from(relay);
let hashed_name = Sha256::digest(name.as_bytes()); let hashed_name = Sha256::digest(name.as_bytes());
let hashed_string = hex::encode(hashed_name); let hashed_string = hex::encode(hashed_name);
// Create a new HTTP client
let client = Client::new(); let client = Client::new();
// Send a POST request to the relay server with the file name hash as a query parameter
let _ = client let _ = client
.post(format!("{}/download_success/{}", url, hashed_string)) .post(format!("{}/download_success/{}", url, hashed_string))
.send() .send()
.await .await
// If the request fails, return an error with the reason
.map_err(|e| anyhow!("Failed to send POST request: {}", e))?; .map_err(|e| anyhow!("Failed to send POST request: {}", e))?;
// Return Ok(()) if the request was successful
Ok(()) Ok(())
} }

View file

@ -10,6 +10,23 @@ use tokio_tungstenite::{
}; };
use tracing::{debug, error}; use tracing::{debug, error};
/// Start the receiver process.
///
/// This function initiates the receiver process by performing the following steps:
/// 1. Replaces the protocol of the given `relay` URL.
/// 2. Downloads the room information from the server.
/// 3. Connects to the local or relay server based on the platform.
/// 4. Downloads the file from the server.
///
/// # Arguments
///
/// * `filepath` - The path to the file to be received.
/// * `relay` - The URL of the relay server.
/// * `name` - The name of the receiver.
///
/// # Returns
///
/// Returns a `Result` indicating the success or failure of the receiver process.
pub async fn start_receiver(filepath: String, relay: &str, name: &str) -> Result<()> { pub async fn start_receiver(filepath: String, relay: &str, name: &str) -> Result<()> {
let http_url = replace_protocol(relay); let http_url = replace_protocol(relay);
let res = http_client::download_info(http_url.as_str(), name) let res = http_client::download_info(http_url.as_str(), name)
@ -44,33 +61,55 @@ pub async fn start_receiver(filepath: String, relay: &str, name: &str) -> Result
Ok(()) Ok(())
} }
/// Asynchronously starts a WebSocket communication with a relay server.
///
/// # Arguments
///
/// * `filepath` - The path of the file to transfer.
/// * `relay` - The URL of the relay server.
/// * `name` - The name of the receiver.
///
/// # Returns
///
/// Returns a `Result` indicating the success or failure of the WebSocket communication.
pub async fn start_ws_com(filepath: String, relay: &str, name: &str) -> Result<()> { pub async fn start_ws_com(filepath: String, relay: &str, name: &str) -> Result<()> {
// Construct the WebSocket URL by appending "/ws" to the relay URL.
let url = String::from(relay) + "/ws"; let url = String::from(relay) + "/ws";
// Create a WebSocket request using the constructed URL.
let mut request = url let mut request = url
.into_client_request() .into_client_request()
.map_err(|e| anyhow!("Failed to create request: {}", e))?; .map_err(|e| anyhow!("Failed to create request: {}", e))?;
// Set the "Origin" header of the request to the relay URL.
request request
.headers_mut() .headers_mut()
.insert("Origin", HeaderValue::from_str(relay).unwrap()); .insert("Origin", HeaderValue::from_str(relay).unwrap());
// Print a message indicating the attempt to connect.
println!("Attempting to connect..."); println!("Attempting to connect...");
// Attempt to establish a WebSocket connection with the relay server.
// If the connection fails or times out, return an error.
let _ = match tokio::time::timeout(std::time::Duration::from_secs(5), connect_async(request)) let _ = match tokio::time::timeout(std::time::Duration::from_secs(5), connect_async(request))
.await .await
{ {
Ok(Ok((socket, _))) => { Ok(Ok((socket, _))) => {
// Start the receiver process with the established WebSocket connection.
receiver::start(filepath, socket, name).await; receiver::start(filepath, socket, name).await;
Ok(()) Ok(())
} }
Ok(Err(e)) => { Ok(Err(e)) => {
// Log the failure to connect.
error!("Error: Failed to connect: {e:?}"); error!("Error: Failed to connect: {e:?}");
Err(Box::new(e)) Err(Box::new(e))
} }
Err(e) => { Err(e) => {
// Log the timeout.
error!("Error: Timeout reached for local connection attempt"); error!("Error: Timeout reached for local connection attempt");
Err(Box::new(e)) Err(Box::new(e))
}?, }?,
}; };
Ok(()) Ok(())
} }

View file

@ -4,18 +4,38 @@ use tokio::sync::RwLock;
use crate::relay::room::Room; use crate::relay::room::Room;
use crate::relay::transfer::TransferResponse; use crate::relay::transfer::TransferResponse;
/// State of the application.
///
/// This structure holds the state of the application, which includes the rooms
/// and the transfers.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppState { pub struct AppState {
/// Map of rooms, where the key is the room's ID and the value is the room
/// itself.
pub rooms: HashMap<String, Room>, pub rooms: HashMap<String, Room>,
/// Vector of transfers.
pub transfers: Vec<TransferResponse>, pub transfers: Vec<TransferResponse>,
} }
impl AppState { impl AppState {
/// Creates a new instance of the `AppState` struct.
///
/// This function initializes the state of the application with an empty map
/// of rooms and an empty vector of transfers.
///
/// # Returns
///
/// An `Arc<RwLock<AppState>>` that can be used to share the state across multiple
/// tasks.
pub fn new() -> Arc<RwLock<AppState>> { pub fn new() -> Arc<RwLock<AppState>> {
Arc::new(RwLock::new(AppState { // Create a new instance of `AppState` with empty rooms and transfers.
let app_state = AppState {
rooms: HashMap::new(), rooms: HashMap::new(),
transfers: Vec::new(), transfers: Vec::new(),
})) };
// Wrap the `app_state` in a `RwLock` to make it thread-safe.
Arc::new(RwLock::new(app_state))
} }
} }

View file

@ -10,43 +10,111 @@ use crate::relay::RequestPacket;
use crate::relay::ResponsePacket; use crate::relay::ResponsePacket;
use uuid::Uuid; use uuid::Uuid;
/// Type alias for a synchronized WebSocket sender.
///
/// This is used to send messages to a WebSocket connection.
type Sender = Arc<Mutex<SplitSink<axum::extract::ws::WebSocket, Message>>>; type Sender = Arc<Mutex<SplitSink<axum::extract::ws::WebSocket, Message>>>;
/// Struct representing a WebSocket client.
///
/// This struct contains a WebSocket sender and an optional room ID.
/// The sender is used to send messages to the WebSocket connection,
/// while the room ID is used to identify the client's room.
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
/// The WebSocket sender for sending messages.
sender: Sender, sender: Sender,
/// The optional room ID of the client.
///
/// This is used to identify the client's room.
room_id: Option<String>, room_id: Option<String>,
} }
impl Client { impl Client {
/// Creates a new WebSocket client.
///
/// # Arguments
///
/// * `sender` - A synchronized WebSocket sender.
///
/// # Returns
///
/// A new WebSocket client instance.
pub fn new(sender: Sender) -> Client { pub fn new(sender: Sender) -> Client {
Client { Client {
sender, sender, // The WebSocket sender for sending messages.
room_id: None, room_id: None, // The optional room ID of the client. This is used to identify the client's room.
} }
} }
/// Sends a message to the WebSocket connection.
///
/// # Arguments
///
/// * `sender` - A synchronized WebSocket sender.
/// * `message` - The message to send.
///
/// # Errors
///
/// If the message fails to be sent.
async fn send(&self, sender: Sender, message: Message) { async fn send(&self, sender: Sender, message: Message) {
let mut sender = sender.lock().await; let mut sender = sender.lock().await; // Acquires a lock on the sender.
if let Err(error) = sender.send(message).await { if let Err(error) = sender.send(message).await { // Sends the message.
error!("Failed to send message to the client: {}", error); error!("Failed to send message to the client: {}", error); // Logs the error if the message fails to be sent.
} }
} }
/// Sends a serialized packet to the WebSocket connection.
///
/// # Arguments
///
/// * `sender` - A synchronized WebSocket sender.
/// * `packet` - The packet to send.
///
/// # Errors
///
/// If the serialized packet fails to be sent.
async fn send_packet(&self, sender: Sender, packet: ResponsePacket) { async fn send_packet(&self, sender: Sender, packet: ResponsePacket) {
// Serialize the packet to a string.
let serialized_packet = serde_json::to_string(&packet).unwrap(); let serialized_packet = serde_json::to_string(&packet).unwrap();
// Send the serialized packet as a text message.
self.send(sender, Message::Text(serialized_packet)).await; self.send(sender, Message::Text(serialized_packet)).await;
} }
/// Sends an error message to the WebSocket connection.
///
/// # Arguments
///
/// * `sender` - A synchronized WebSocket sender.
/// * `message` - The error message to send.
///
/// # Errors
///
/// If the error message fails to be sent.
async fn send_error_packet(&self, sender: Sender, message: String) { async fn send_error_packet(&self, sender: Sender, message: String) {
// Create an error packet with the given message.
let error_packet = ResponsePacket::Error { message }; let error_packet = ResponsePacket::Error { message };
self.send_packet(sender, error_packet).await // Send the error packet.
self.send_packet(sender, error_packet).await;
} }
/// Handles the "create_room" request from a client.
///
/// # Arguments
///
/// * `server` - A lock guard of the `AppState`.
/// * `id` - An optional string representing the room identifier.
///
/// # Errors
///
/// If the room already exists or if the room creation fails.
async fn handle_create_room(&mut self, server: &RwLock<AppState>, id: Option<String>) { async fn handle_create_room(&mut self, server: &RwLock<AppState>, id: Option<String>) {
// Acquire a write lock on the server state.
let mut server = server.write().await; let mut server = server.write().await;
// Check if the client is already in a room.
if server.rooms.iter().any(|(_, room)| { if server.rooms.iter().any(|(_, room)| {
room.senders room.senders
.iter() .iter()
@ -55,14 +123,16 @@ impl Client {
return; return;
} }
// Set the room size and generate a room identifier if none is provided.
let size = Room::DEFAULT_ROOM_SIZE; let size = Room::DEFAULT_ROOM_SIZE;
let room_id = match id { let room_id = match id {
Some(id) => id, Some(id) => id,
None => Uuid::new_v4().to_string(), None => Uuid::new_v4().to_string(),
}; };
// Check if the room identifier already exists.
if server.rooms.contains_key(&room_id) { if server.rooms.contains_key(&room_id) {
drop(server); drop(server); // Release the lock before returning.
return self return self
.send_error_packet( .send_error_packet(
@ -72,23 +142,37 @@ impl Client {
.await; .await;
} }
// Create a new room and add the client to it.
let mut room = Room::new(size); let mut room = Room::new(size);
room.senders.push(self.sender.clone()); room.senders.push(self.sender.clone());
// Insert the room into the server state.
server.rooms.insert(room_id.clone(), room); server.rooms.insert(room_id.clone(), room);
self.room_id = Some(room_id.clone()); self.room_id = Some(room_id.clone()); // Store the room identifier.
drop(server); drop(server); // Release the lock before returning.
debug!("Room created"); debug!("Room created");
// Send the response packet to the client.
self.send_packet(self.sender.clone(), ResponsePacket::Create { id: room_id }) self.send_packet(self.sender.clone(), ResponsePacket::Create { id: room_id })
.await .await
} }
/// Handles the "join_room" request from a client.
///
/// # Arguments
///
/// * `server` - A lock guard of the `AppState`.
/// * `room_id` - A string representing the room identifier.
///
/// # Errors
///
/// If the room does not exist or if the room is full.
async fn handle_join_room(&mut self, server: &RwLock<AppState>, room_id: String) { async fn handle_join_room(&mut self, server: &RwLock<AppState>, room_id: String) {
let mut server = server.write().await; let mut server = server.write().await;
// Check if the client is already in a room.
if server.rooms.iter().any(|(_, room)| { if server.rooms.iter().any(|(_, room)| {
room.senders room.senders
.iter() .iter()
@ -100,24 +184,29 @@ impl Client {
let Some(room) = server.rooms.get_mut(&room_id) else { let Some(room) = server.rooms.get_mut(&room_id) else {
drop(server); drop(server);
// Send an error packet to the client.
return self return self
.send_error_packet(self.sender.clone(), "The room does not exist.".to_string()) .send_error_packet(self.sender.clone(), "The room does not exist.".to_string())
.await; .await;
}; };
// Check if the room is full.
if room.senders.len() >= room.size { if room.senders.len() >= room.size {
drop(server); drop(server);
// Send an error packet to the client.
return self return self
.send_error_packet(self.sender.clone(), "The room is full.".to_string()) .send_error_packet(self.sender.clone(), "The room is full.".to_string())
.await; .await;
} }
// Add the client to the room.
room.senders.push(self.sender.clone()); room.senders.push(self.sender.clone());
self.room_id = Some(room_id); self.room_id = Some(room_id);
let mut futures = vec![]; let mut futures = vec![];
for sender in &room.senders { for sender in &room.senders {
// Send a join packet to the client with its position in the room.
if Arc::ptr_eq(sender, &self.sender) { if Arc::ptr_eq(sender, &self.sender) {
futures.push(self.send_packet( futures.push(self.send_packet(
sender.clone(), sender.clone(),
@ -126,6 +215,7 @@ impl Client {
}, },
)); ));
} else { } else {
// Send a join packet to the client without its position in the room.
futures.push(self.send_packet(sender.clone(), ResponsePacket::Join { size: None })); futures.push(self.send_packet(sender.clone(), ResponsePacket::Join { size: None }));
} }
} }
@ -134,17 +224,34 @@ impl Client {
join_all(futures).await; join_all(futures).await;
} }
/// Handle the leave room request from the client.
///
/// This function removes the client from the current room and notifies the other
/// clients in the room about the client's departure.
///
/// # Arguments
///
/// * `server` - A read-write lock guard for the server state.
///
/// # Returns
///
/// This function does not return anything.
#[allow(clippy::needless_pass_by_value)]
async fn handle_leave_room(&mut self, server: &RwLock<AppState>) { async fn handle_leave_room(&mut self, server: &RwLock<AppState>) {
// Acquire a write lock on the server state.
let mut server = server.write().await; let mut server = server.write().await;
// Get the room ID of the current room.
let Some(room_id) = self.room_id.clone() else { let Some(room_id) = self.room_id.clone() else {
return; return;
}; };
// Get the mutable reference to the room.
let Some(room) = server.rooms.get_mut(&room_id) else { let Some(room) = server.rooms.get_mut(&room_id) else {
return; return;
}; };
// Get the index of the client in the room.
let Some(index) = room let Some(index) = room
.senders .senders
.iter() .iter()
@ -153,70 +260,94 @@ impl Client {
return; return;
}; };
// Remove the client from the room.
room.senders.remove(index); room.senders.remove(index);
self.room_id = None; self.room_id = None;
let mut futures = vec![]; let mut futures = vec![];
for sender in &room.senders { for sender in &room.senders {
// Send a leave packet to the other clients in the room.
futures.push(self.send_packet(sender.clone(), ResponsePacket::Leave { index })); futures.push(self.send_packet(sender.clone(), ResponsePacket::Leave { index }));
} }
// If the room is empty, remove it from the server state.
if room.senders.is_empty() { if room.senders.is_empty() {
server.rooms.remove(&room_id); server.rooms.remove(&room_id);
} }
drop(server); drop(server);
// Wait for all the futures to complete.
join_all(futures).await; join_all(futures).await;
} }
/// Handles incoming messages from the client.
///
/// This function interprets the incoming message and performs the corresponding action.
///
/// # Arguments
///
/// * `server` - A RwLock guard containing the state of the server.
/// * `message` - The incoming message from the client.
pub async fn handle_message(&mut self, server: &RwLock<AppState>, message: Message) { pub async fn handle_message(&mut self, server: &RwLock<AppState>, message: Message) {
// Match on the type of the message.
match message { match message {
// If the message is text, parse it as a RequestPacket.
Message::Text(text) => { Message::Text(text) => {
let packet = match serde_json::from_str(&text) { let packet = match serde_json::from_str(&text) {
Ok(packet) => packet, Ok(packet) => packet,
Err(_) => return, Err(_) => return, // Return if the parsing fails.
}; };
// Match on the RequestPacket type and perform the corresponding action.
match packet { match packet {
RequestPacket::Create { id } => self.handle_create_room(server, id).await, RequestPacket::Create { id } => self.handle_create_room(server, id).await,
RequestPacket::Join { id } => self.handle_join_room(server, id).await, RequestPacket::Join { id } => self.handle_join_room(server, id).await,
RequestPacket::Leave => self.handle_leave_room(server).await, RequestPacket::Leave => self.handle_leave_room(server).await,
} }
} }
// If the message is binary, handle it accordingly.
Message::Binary(_) => { Message::Binary(_) => {
// Acquire a read lock on the server state.
let server = server.read().await; let server = server.read().await;
// Get the room ID of the current room.
let Some(room_id) = &self.room_id else { let Some(room_id) = &self.room_id else {
drop(server); drop(server);
return; return; // Return if the client is not in a room.
}; };
// Get the room corresponding to the room ID.
let Some(room) = server.rooms.get(room_id) else { let Some(room) = server.rooms.get(room_id) else {
drop(server); drop(server);
return; return; // Return if the room does not exist.
}; };
// Get the index of the client in the room.
let Some(index) = room let Some(index) = room
.senders .senders
.iter() .iter()
.position(|sender| Arc::ptr_eq(sender, &self.sender)) .position(|sender| Arc::ptr_eq(sender, &self.sender))
else { else {
drop(server); drop(server);
return; return; // Return if the client is not in the room.
}; };
// Get the binary data from the message.
let mut data = message.into_data(); let mut data = message.into_data();
if data.is_empty() { if data.is_empty() {
drop(server); drop(server);
return; return; // Return if the data is empty.
} }
// Convert the index to a u8 and assign it as the source.
let source = u8::try_from(index).unwrap(); let source = u8::try_from(index).unwrap();
// Get the destination from the first byte of the data.
let destination = usize::from(data[0]); let destination = usize::from(data[0]);
data[0] = source; data[0] = source; // Assign the source to the first byte of the data.
// If the destination is within the range of the room senders, send the data to that sender.
if destination < room.senders.len() { if destination < room.senders.len() {
let sender = room.senders[destination].clone(); let sender = room.senders[destination].clone();
@ -224,11 +355,12 @@ impl Client {
return self.send(sender, Message::Binary(data)).await; return self.send(sender, Message::Binary(data)).await;
} }
// If the destination is u8::MAX, send the data to all the room senders except the current one.
if destination == usize::from(u8::MAX) { if destination == usize::from(u8::MAX) {
let mut futures = vec![]; let mut futures = vec![];
for sender in &room.senders { for sender in &room.senders {
if Arc::ptr_eq(sender, &self.sender) { if Arc::ptr_eq(sender, &self.sender) {
continue; continue; // Skip the current client.
} }
futures.push(self.send(sender.clone(), Message::Binary(data.clone()))); futures.push(self.send(sender.clone(), Message::Binary(data.clone())));
@ -238,12 +370,15 @@ impl Client {
join_all(futures).await; join_all(futures).await;
} }
} }
// If the message is Ping, print a message.
Message::Ping(_) => { Message::Ping(_) => {
println!("Got Message Type Ping"); println!("Got Message Type Ping");
} }
// If the message is Pong, print a message.
Message::Pong(_) => { Message::Pong(_) => {
println!("Got Message Type Pong"); println!("Got Message Type Pong");
} }
// If the message is Close, print a message and handle the close.
Message::Close(_) => { Message::Close(_) => {
println!("Got Message Type Close"); println!("Got Message Type Close");
self.handle_close(server).await; self.handle_close(server).await;

View file

@ -6,33 +6,75 @@ pub mod transfer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Represents a packet sent by a client to the server.
///
/// The `type` field is used to determine the type of the packet. It can be one of the following
/// values:
/// - `Join`: The client wants to join a room.
/// - `Create`: The client wants to create a new room.
/// - `Leave`: The client wants to leave the current room.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum RequestPacket { pub enum RequestPacket {
/// The client wants to join a room.
///
/// The `id` field is the ID of the room that the client wants to join.
Join { Join {
// The ID of the room that the client wants to join. /// The ID of the room that the client wants to join.
id: String, id: String,
}, },
/// The client wants to create a new room.
///
/// The `id` field is an optional field that specifies the ID of the new room. If it is `None`,
/// a random ID will be generated.
Create { Create {
/// The ID of the new room. If it is `None`, a random ID will be generated.
id: Option<String>, id: Option<String>,
}, },
/// The client wants to leave the current room.
Leave, Leave,
} }
/// Represents a packet sent by the server to the client.
///
/// The `type` field is used to determine the type of the packet. It can be one of the following
/// values:
/// - `Join`: The client has joined a room.
/// - `Create`: The client has created a new room.
/// - `Leave`: The client has left the current room.
/// - `Error`: There was an error.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum ResponsePacket { pub enum ResponsePacket {
/// The client has joined a room.
///
/// The `size` field is an optional field that specifies the size of the room. If it is `None`,
/// the size is unknown.
Join { Join {
/// The size of the room. If it is `None`, the size is unknown.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
size: Option<usize>, size: Option<usize>,
}, },
/// The client has created a new room.
///
/// The `id` field is the ID of the new room.
Create { Create {
/// The ID of the new room.
id: String, id: String,
}, },
/// The client has left the current room.
///
/// The `index` field is the index of the client in the room.
Leave { Leave {
/// The index of the client in the room.
index: usize, index: usize,
}, },
/// There was an error.
///
/// The `message` field is the error message.
Error { Error {
/// The error message.
message: String, message: String,
}, },
} }

View file

@ -3,17 +3,43 @@ use futures_util::stream::SplitSink;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
// `Sender` is a type alias for a synchronized WebSocket sender.
//
// This is used to send messages to a WebSocket connection.
type Sender = Arc<Mutex<SplitSink<WebSocket, Message>>>; type Sender = Arc<Mutex<SplitSink<WebSocket, Message>>>;
/// Struct representing a room of WebSocket clients.
///
/// A `Room` contains a list of WebSocket senders and a room size.
/// The senders are used to send messages to the WebSocket connections,
/// while the room size represents the maximum number of clients allowed in the room.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Room { pub struct Room {
/// The list of WebSocket senders.
///
/// Each sender is used to send messages to a WebSocket connection.
pub senders: Vec<Sender>, pub senders: Vec<Sender>,
/// The size of the room.
///
/// This represents the maximum number of clients allowed in the room.
pub size: usize, pub size: usize,
} }
impl Room { impl Room {
/// The default room size.
///
/// This is used as a fallback value when creating a new room.
pub const DEFAULT_ROOM_SIZE: usize = 2; pub const DEFAULT_ROOM_SIZE: usize = 2;
/// Create a new room with the specified size.
///
/// # Arguments
///
/// * `size` - The size of the room.
///
/// # Returns
///
/// A new `Room` instance.
pub fn new(size: usize) -> Room { pub fn new(size: usize) -> Room {
Room { Room {
senders: Vec::new(), senders: Vec::new(),

View file

@ -21,29 +21,45 @@ use crate::relay::client::Client;
use crate::relay::transfer::TransferResponse; use crate::relay::transfer::TransferResponse;
use crate::relay::{appstate::AppState, transfer::TransferRequest}; use crate::relay::{appstate::AppState, transfer::TransferRequest};
/// Start the WebSocket server.
///
/// This function initializes the server and starts listening for incoming connections.
/// It configures the routes for the WebSocket handler and the upload and download routes.
/// Additionally, it sets up the tracing layer to log incoming requests.
///
/// # Arguments
///
/// * `port` - The port number to listen on.
/// * `listen_addr` - The IP address to listen on.
#[allow(clippy::unused_self)]
pub async fn start_ws(port: &i32, listen_addr: &String) { pub async fn start_ws(port: &i32, listen_addr: &String) {
let app_host = listen_addr; // Log the server configuration.
let app_port = port; debug!("Server configured to accept connections on host {listen_addr}...");
debug!("Server configured to listen connections on port {port}...");
debug!("Server configured to accept connections on host {app_host}...",);
debug!("Server configured to listen connections on port {app_port}...",);
// Create a new instance of the server state.
let server = AppState::new(); let server = AppState::new();
// Set up the routes for the server.
let app = Router::new() let app = Router::new()
.route("/ws", get(ws_handler)) .route("/ws", get(ws_handler))
.route("/upload", put(upload_info)) .route("/upload", put(upload_info))
.route("/download/:name", get(download_info)) .route("/download/:name", get(download_info))
.route("/download_success/:name", post(download_success)) .route("/download_success/:name", post(download_success))
.with_state(server) .with_state(server)
// Set up the tracing layer to log incoming requests.
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::default().include_headers(true)), .make_span_with(DefaultMakeSpan::default().include_headers(true)),
); );
if let Ok(listener) = TcpListener::bind(&format!("{}:{}", app_host, app_port)).await { // Start listening for incoming connections.
info!("Listening on: {}", listener.local_addr().unwrap()); let addr = format!("{}:{}", listen_addr, port);
if let Ok(listener) = TcpListener::bind(&addr).await {
let local_addr = listener.local_addr().unwrap();
info!("Listening on: {}", local_addr);
// Serve the app with the listener and handle shutdown gracefully.
axum::serve( axum::serve(
listener, listener,
app.into_make_service_with_connect_info::<SocketAddr>(), app.into_make_service_with_connect_info::<SocketAddr>(),
@ -52,35 +68,68 @@ pub async fn start_ws(port: &i32, listen_addr: &String) {
.await .await
.unwrap(); .unwrap();
} else { } else {
error!("Failed to listen on: {}:{}", app_host, app_port); // Log if the server failed to start.
error!("Failed to listen on: {listen_addr}:{port}");
} }
} }
/// Handler for the WebSocket route.
///
/// This function upgrades the connection to a WebSocket and handles the socket.
///
/// # Arguments
///
/// * `ws` - The WebSocketUpgrade struct containing the upgrade request.
/// * `shared_state` - The shared state of the server.
pub async fn ws_handler( pub async fn ws_handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
State(shared_state): State<Arc<RwLock<AppState>>>, State(shared_state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// Log the request on the WebSocket route.
debug!("Got Request on Websocket route"); debug!("Got Request on Websocket route");
// Log the connection upgrade.
debug!("Upgrading Connection"); debug!("Upgrading Connection");
// Upgrade the connection to a WebSocket and handle the socket.
// Move the shared state to the handler to avoid holding the lock during the entire connection.
ws.on_upgrade(move |socket| handle_socket(socket, shared_state)) ws.on_upgrade(move |socket| handle_socket(socket, shared_state))
} }
/// Handles the WebSocket connection.
///
/// This function splits the WebSocket into a sender and receiver,
/// creates a client, and handles the messages received from the client.
/// It also handles the close event from the client.
///
/// # Arguments
///
/// * `socket` - The WebSocket connection.
/// * `rooms` - The shared state of the server.
async fn handle_socket(socket: WebSocket, rooms: Arc<RwLock<AppState>>) { async fn handle_socket(socket: WebSocket, rooms: Arc<RwLock<AppState>>) {
// Split the WebSocket into a sender and receiver.
let (sender, mut receiver) = socket.split(); let (sender, mut receiver) = socket.split();
// Create a new Mutex to prevent concurrent access to the sender.
let sender = Arc::new(Mutex::new(sender)); let sender = Arc::new(Mutex::new(sender));
// Create a new client with the sender.
let mut client = Client::new(sender.clone()); let mut client = Client::new(sender.clone());
// Handle the messages received from the client.
while let Some(message) = receiver.next().await { while let Some(message) = receiver.next().await {
match message { match message {
Ok(message) => { Ok(message) => {
// Handle the message received from the client.
client.handle_message(&rooms, message).await; client.handle_message(&rooms, message).await;
} }
Err(error) => { Err(error) => {
// Log the error if failed to read message from the client.
warn!("Failed to read message from client: {}", error); warn!("Failed to read message from client: {}", error);
break; break;
} }
} }
} }
// Handle the close event from the client. // Handle the close event from the client.
client.handle_close(&rooms).await client.handle_close(&rooms).await
} }
@ -109,16 +158,34 @@ async fn shutdown_signal() {
} }
} }
/// Handles the upload_info route.
///
/// It updates or creates a new transfer request in the shared state.
/// If the request is found in the shared state, it updates the relay_room_id or local_room_id
/// based on the payload. If the request is not found, it creates a new transfer request and
/// adds it to the shared state.
///
/// # Arguments
///
/// * `shared_state` - The shared state containing the transfer requests.
/// * `payload` - The JSON payload containing the transfer request information.
///
/// # Returns
///
/// A tuple of the HTTP status code and the JSON response.
pub async fn upload_info( pub async fn upload_info(
State(shared_state): State<Arc<RwLock<AppState>>>, State(shared_state): State<Arc<RwLock<AppState>>>,
Json(payload): Json<TransferRequest>, Json(payload): Json<TransferRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut data = shared_state.write().await; let mut data = shared_state.write().await;
// Find the transfer request in the shared state
match data match data
.transfers .transfers
.iter_mut() .iter_mut()
.find(|request| request.name == payload.name) .find(|request| request.name == payload.name)
{ {
// Update the relay_room_id or local_room_id if the request is found
Some(request) => { Some(request) => {
debug!("Found Transfer"); debug!("Found Transfer");
debug!("Request is: {:?}", request); debug!("Request is: {:?}", request);
@ -134,7 +201,9 @@ pub async fn upload_info(
(StatusCode::OK, Json(request.clone())) (StatusCode::OK, Json(request.clone()))
} }
} }
// Create a new transfer request if the request is not found
None => { None => {
// Initialize relay and local room IDs based on the payload
let mut local = String::from(""); let mut local = String::from("");
let mut relay = String::from(""); let mut relay = String::from("");
if payload.relay_room_id.is_empty() { if payload.relay_room_id.is_empty() {
@ -142,37 +211,58 @@ pub async fn upload_info(
} else { } else {
relay = payload.relay_room_id; relay = payload.relay_room_id;
} }
// Create a new transfer request
let t_request = TransferResponse { let t_request = TransferResponse {
name: payload.name, name: payload.name,
ip: payload.ip, ip: payload.ip,
local_room_id: local, local_room_id: local,
relay_room_id: relay, relay_room_id: relay,
}; };
// Add the transfer request to the shared state
data.transfers.push(t_request.clone()); data.transfers.push(t_request.clone());
debug!("New TransferRequest created"); debug!("New TransferRequest created");
debug!("Actual AppState is {:#?}", *data); debug!("Actual AppState is {:#?}", *data);
// Return the created transfer request as the response
(StatusCode::CREATED, Json(t_request)) (StatusCode::CREATED, Json(t_request))
} }
} }
} }
/// Retrieve information about a transfer request based on the transfer name.
///
/// # Arguments
///
/// * `shared_state` - The shared state containing the transfer requests.
/// * `name` - The name of the transfer request.
///
/// # Returns
///
/// Returns a response containing the transfer request if found, or a not found
/// response if the transfer request is not found.
pub async fn download_info( pub async fn download_info(
State(shared_state): State<Arc<RwLock<AppState>>>, State(shared_state): State<Arc<RwLock<AppState>>>,
Path(name): Path<String>, Path(name): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// Acquire write lock on shared state
let data = shared_state.write().await; let data = shared_state.write().await;
// Find transfer request by name
match data.transfers.iter().find(|request| request.name == name) { match data.transfers.iter().find(|request| request.name == name) {
// If transfer request is found, return Ok response with the transfer request
Some(request) => { Some(request) => {
debug!("Found transfer name."); debug!("Found transfer name.");
(StatusCode::OK, Json(request.clone())) (StatusCode::OK, Json(request.clone()))
} }
// If transfer request is not found, return not found response
None => { None => {
warn!("couldn't find transfer-name: {}", name); warn!("couldn't find transfer-name: {}", name);
( (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
Json(TransferResponse { Json(TransferResponse {
// Create a new empty transfer response
name: String::from(""), name: String::from(""),
ip: String::from(""), ip: String::from(""),
local_room_id: String::from(""), local_room_id: String::from(""),
@ -183,19 +273,34 @@ pub async fn download_info(
} }
} }
/// Delete a transfer request by its name.
///
/// # Arguments
///
/// * `shared_state` - The shared state containing the transfer requests.
/// * `name` - The name of the transfer request.
///
/// # Returns
///
/// Returns a response containing a JSON object with a message indicating the
/// success of the deletion. If the transfer request is not found, a not found
/// response is returned.
pub async fn download_success( pub async fn download_success(
State(shared_state): State<Arc<RwLock<AppState>>>, State(shared_state): State<Arc<RwLock<AppState>>>,
Path(name): Path<String>, Path(name): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let mut data = shared_state.write().await; let mut data = shared_state.write().await;
// Find the index of the transfer request
if let Some(index) = data if let Some(index) = data
.transfers .transfers
.iter() .iter()
.position(|request| request.name == name) .position(|request| request.name == name)
{ {
// If the transfer request is found, remove it from the shared state
debug!("Found Transfer by name '{name}'"); debug!("Found Transfer by name '{name}'");
data.transfers.remove(index); data.transfers.remove(index);
debug!("Transfer deleted"); debug!("Transfer deleted");
// Return a success response
( (
StatusCode::OK, StatusCode::OK,
Json(json!({ Json(json!({
@ -203,6 +308,7 @@ pub async fn download_success(
})), })),
) )
} else { } else {
// If the transfer request is not found, return a not found response
warn!("couldn't find transfer-name: {}", name); warn!("couldn't find transfer-name: {}", name);
( (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,

View file

@ -1,13 +1,31 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Request to transfer a connection from one relay to another
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransferRequest { pub struct TransferRequest {
/// The name of the client
pub name: String, pub name: String,
/// The IP address of the client
pub ip: String, pub ip: String,
/// The local room ID of the client
pub local_room_id: String, pub local_room_id: String,
/// The relay room ID of the client
pub relay_room_id: String, pub relay_room_id: String,
} }
impl TransferRequest { impl TransferRequest {
/// Creates a new transfer request
///
/// # Args
///
/// * `name` - The name of the client
/// * `ip` - The IP address of the client
/// * `local_room_id` - The local room ID of the client
/// * `relay_room_id` - The relay room ID of the client
///
/// # Returns
///
/// A new `TransferRequest` instance
pub fn new(name: String, ip: String, local_room_id: String, relay_room_id: String) -> Self { pub fn new(name: String, ip: String, local_room_id: String, relay_room_id: String) -> Self {
Self { Self {
name, name,
@ -18,15 +36,32 @@ impl TransferRequest {
} }
} }
/// Response containing the details of the transferred connection
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransferResponse { pub struct TransferResponse {
/// The name of the client
pub name: String, pub name: String,
/// The IP address of the client
pub ip: String, pub ip: String,
/// The local room ID of the client
pub local_room_id: String, pub local_room_id: String,
/// The relay room ID of the client
pub relay_room_id: String, pub relay_room_id: String,
} }
impl TransferResponse { impl TransferResponse {
/// Creates a new transfer response
///
/// # Args
///
/// * `name` - The name of the client
/// * `ip` - The IP address of the client
/// * `local_room_id` - The local room ID of the client
/// * `relay_room_id` - The relay room ID of the client
///
/// # Returns
///
/// A new `TransferResponse` instance
pub fn new(name: String, ip: String, local_room_id: String, relay_room_id: String) -> Self { pub fn new(name: String, ip: String, local_room_id: String, relay_room_id: String) -> Self {
Self { Self {
name, name,

View file

@ -31,22 +31,57 @@ const NONCE_SIZE: usize = 12;
const MAX_CHUNK_SIZE: isize = u16::MAX as isize; const MAX_CHUNK_SIZE: isize = u16::MAX as isize;
const DELAY: Duration = Duration::from_millis(750); const DELAY: Duration = Duration::from_millis(750);
/// Struct representing a file to be sent.
///
/// This struct holds the path, name and size of a file.
#[derive(Clone)] #[derive(Clone)]
struct File { struct File {
/// The path of the file to be sent.
path: String, path: String,
/// The name of the file to be sent.
name: String, name: String,
/// The size of the file to be sent.
size: u64, size: u64,
} }
/// The context of a sender.
///
/// This struct holds the necessary information for a sender to send files.
/// It includes the HMAC, the sender, the ephemeral secret, the list of files to
/// be sent, the shared key, and the task handling the sending of the files.
struct Context { struct Context {
/// The HMAC used for authentication.
hmac: Vec<u8>, hmac: Vec<u8>,
/// The sender used to send packets.
sender: Sender, sender: Sender,
/// The ephemeral secret used for key exchange.
key: EphemeralSecret, key: EphemeralSecret,
/// The list of files to be sent.
files: Vec<File>, files: Vec<File>,
/// The shared key used for encryption.
shared_key: Option<Aes128Gcm>, shared_key: Option<Aes128Gcm>,
/// The task handling the sending of the files.
task: Option<JoinHandle<()>>, task: Option<JoinHandle<()>>,
} }
/// Handles the create room packet.
///
/// This function is called when a create room packet is received.
/// It creates a room on the specified relay and sends the necessary
/// information to the server.
///
/// # Arguments
///
/// * `context` - The context of the sender.
/// * `id` - The ID of the room.
/// * `relay` - The URL of the relay.
/// * `transfer_name` - The name of the transfer.
/// * `is_local` - A boolean indicating whether the room is local.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
fn on_create_room( fn on_create_room(
context: &Context, context: &Context,
id: String, id: String,
@ -54,16 +89,26 @@ fn on_create_room(
transfer_name: String, transfer_name: String,
is_local: bool, is_local: bool,
) -> Status { ) -> Status {
// Debug log the relay URL
debug!("Creating room on: {relay}"); debug!("Creating room on: {relay}");
// Encode the HMAC key using base64
let base64 = general_purpose::STANDARD.encode(&context.hmac); let base64 = general_purpose::STANDARD.encode(&context.hmac);
// Generate the URL for the room
let url = format!("{}-{}", id, base64); let url = format!("{}-{}", id, base64);
// Hash the transfer name
let hash_name = hash_random_name(transfer_name.clone()); let hash_name = hash_random_name(transfer_name.clone());
// Create copies of the necessary variables for the thread
let send_url = url.to_string(); let send_url = url.to_string();
let h_name = hash_name.to_string(); let h_name = hash_name.to_string();
let server_url = replace_protocol(relay.as_str()); let server_url = replace_protocol(relay.as_str());
// Spawn a new thread to send the information to the server
let res = std::thread::spawn(move || { let res = std::thread::spawn(move || {
// Create a new runtime and block on the current thread
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
@ -72,14 +117,20 @@ fn on_create_room(
}) })
.join() .join()
.unwrap(); .unwrap();
// Debug log the result
debug!("Got Result: {:?}", res); debug!("Got Result: {:?}", res);
// Handle the result of sending the information to the server
match res { match res {
Ok(transfer_response) => { Ok(transfer_response) => {
// Print the room URL and transfer name
if !transfer_response.local_room_id.is_empty() if !transfer_response.local_room_id.is_empty()
&& !transfer_response.relay_room_id.is_empty() && !transfer_response.relay_room_id.is_empty()
{ {
println!(); println!();
// Print the QR code for the transfer name
if let Err(error) = qr2term::print_qr(&transfer_name) { if let Err(error) = qr2term::print_qr(&transfer_name) {
error!("Failed to generate QR code: {}", error); error!("Failed to generate QR code: {}", error);
} }
@ -90,30 +141,56 @@ fn on_create_room(
} }
} }
Err(e) => { Err(e) => {
// Log the error
error!("Error sending info: {e}"); error!("Error sending info: {e}");
} }
} }
// Continue with the operation
Status::Continue() Status::Continue()
} }
/// Handle the join room packet.
///
/// This function is responsible for handling the join room packet received from the receiver.
/// It checks if the size of the room is provided and returns an error if it is not. It then
/// generates the public key and signs it with the HMAC key. It sends the handshake packet to
/// the receiver.
///
/// # Arguments
///
/// * `context` - The sender context.
/// * `size` - The size of the room.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if the join room packet is invalid.
fn on_join_room(context: &Context, size: Option<usize>) -> Status { fn on_join_room(context: &Context, size: Option<usize>) -> Status {
// Check if the size of the room is provided
if size.is_some() { if size.is_some() {
return Status::Err("Invalid join room packet.".into()); return Status::Err("Invalid join room packet.".into());
} }
// Generate the public key
let public_key = context.key.public_key().to_sec1_bytes().into_vec(); let public_key = context.key.public_key().to_sec1_bytes().into_vec();
// Generate the signature by signing the public key with the HMAC key
let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap(); let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap();
mac.update(&public_key); mac.update(&public_key);
let signature = mac.finalize().into_bytes().to_vec(); let signature = mac.finalize().into_bytes().to_vec();
// Create the handshake packet with the public key and signature
let handshake = HandshakePacket { let handshake = HandshakePacket {
public_key, public_key,
signature, signature,
}; };
// Send the handshake packet to the receiver
context context
.sender .sender
.send_packet(DESTINATION, Value::Handshake(handshake)); .send_packet(DESTINATION, Value::Handshake(handshake));
@ -121,106 +198,215 @@ fn on_join_room(context: &Context, size: Option<usize>) -> Status {
Status::Continue() Status::Continue()
} }
/// Handles errors by returning a `Status` with the error message.
///
/// # Arguments
///
/// * `message` - The error message.
///
/// # Returns
///
/// A `Status` indicating the error with the error message.
#[allow(clippy::missing_panics_doc)]
#[inline]
fn on_error(message: String) -> Status { fn on_error(message: String) -> Status {
// Return a `Status` with the error message
Status::Err(message) Status::Err(message)
} }
/// Handle the leave room packet.
///
/// This function handles the leave room packet by aborting any ongoing task,
/// generating a new random key, clearing the shared key, clearing the task,
/// and printing an error message indicating that the transfer was interrupted
/// because the receiver disconnected.
///
/// # Arguments
///
/// * `context` - The sender context.
/// * `_` - The index of the sender. Currently unused.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if there are still files being transferred.
#[allow(clippy::missing_panics_doc)]
#[inline]
fn on_leave_room(context: &mut Context, _: usize) -> Status { fn on_leave_room(context: &mut Context, _: usize) -> Status {
// Abort any ongoing task
if let Some(task) = &context.task { if let Some(task) = &context.task {
task.abort(); task.abort();
} }
// Generate a new random key
context.key = EphemeralSecret::random(&mut OsRng); context.key = EphemeralSecret::random(&mut OsRng);
// Clear the shared key
context.shared_key = None; context.shared_key = None;
// Clear the task
context.task = None; context.task = None;
// Print an error message
println!(); println!();
error!("Transfer was interrupted because the receiver disconnected."); error!("Transfer was interrupted because the receiver disconnected.");
// Return a `Status` to indicate that the operation was successful
Status::Continue() Status::Continue()
} }
/// Handle the progress packet.
///
/// # Arguments
///
/// * `context` - The sender context.
/// * `progress` - The progress packet.
///
/// # Returns
///
/// A `Status` representing the result of the operation.
///
/// # Errors
///
/// Returns an error if the progress packet is invalid.
#[allow(clippy::missing_panics_doc)]
#[inline]
fn on_progress(context: &Context, progress: ProgressPacket) -> Status { fn on_progress(context: &Context, progress: ProgressPacket) -> Status {
// Check if the shared key is established
if context.shared_key.is_none() { if context.shared_key.is_none() {
// Return an error if the progress packet is invalid
return Status::Err("Invalid progress packet: no shared key established".into()); return Status::Err("Invalid progress packet: no shared key established".into());
} }
// Get the file corresponding to the progress packet index
let file = match context.files.get(progress.index as usize) { let file = match context.files.get(progress.index as usize) {
Some(file) => file, Some(file) => file,
None => return Status::Err("Invalid index in progress packet.".into()), None => return Status::Err("Invalid index in progress packet.".into()),
}; };
// Print the progress of the file transfer
print!("\rTransferring '{}': {}%", file.name, progress.progress); print!("\rTransferring '{}': {}%", file.name, progress.progress);
// Flush the stdout
stdout().flush().unwrap(); stdout().flush().unwrap();
// Check if the progress is 100%
if progress.progress == 100 { if progress.progress == 100 {
// Print a newline
println!(); println!();
// Check if this is the last file being transferred
if progress.index as usize == context.files.len() - 1 { if progress.index as usize == context.files.len() - 1 {
// Return an exit status to indicate that the operation was successful
return Status::Exit(); return Status::Exit();
} }
} }
// Return a continue status to indicate that the operation was successful
Status::Continue() Status::Continue()
} }
async fn on_chunk(sender: Sender, shared_key: Option<Aes128Gcm>, files: Vec<File>) {
/// Asynchronously transfers the chunks of files to the receiver.
///
/// # Arguments
///
/// * `sender` - The sender object used to send packets.
/// * `shared_key` - The shared key used for encryption.
/// * `files` - The list of files to be transferred.
#[allow(clippy::missing_panics_doc)]
#[inline]
async fn on_chunk(
sender: Sender,
shared_key: Option<Aes128Gcm>,
files: Vec<File>,
) {
// For each file in the list of files
for file in files { for file in files {
let mut sequence = 0; let mut sequence = 0;
let mut chunk_size = MAX_CHUNK_SIZE; let mut chunk_size = MAX_CHUNK_SIZE;
let mut size = file.size as isize; let mut size = file.size as isize;
// Open the file
let mut handle = match tokio::fs::File::open(file.path).await { let mut handle = match tokio::fs::File::open(file.path).await {
Ok(handle) => handle, Ok(handle) => handle,
Err(error) => { Err(error) => {
// Print an error message if the file cannot be opened
println!("Error: Unable to open file '{}': {}", file.name, error); println!("Error: Unable to open file '{}': {}", file.name, error);
return; return;
} }
}; };
// While there are still chunks to be transferred
while size > 0 { while size > 0 {
// If the remaining size is less than the maximum chunk size
if size < chunk_size { if size < chunk_size {
// Set the chunk size to the remaining size
chunk_size = size; chunk_size = size;
} }
// Create a vector to hold the chunk
let mut chunk = vec![0u8; chunk_size.try_into().unwrap()]; let mut chunk = vec![0u8; chunk_size.try_into().unwrap()];
// Read the chunk from the file
handle.read_exact(&mut chunk).await.unwrap(); handle.read_exact(&mut chunk).await.unwrap();
// Send the encrypted chunk packet to the receiver
sender.send_encrypted_packet( sender.send_encrypted_packet(
&shared_key, &shared_key,
DESTINATION, DESTINATION,
Value::Chunk(ChunkPacket { sequence, chunk }), Value::Chunk(ChunkPacket { sequence, chunk }),
); );
// Increment the sequence and decrement the size
sequence += 1; sequence += 1;
size -= chunk_size; size -= chunk_size;
} }
// Wait for a delay before starting the next file transfer
sleep(DELAY).await; sleep(DELAY).await;
} }
} }
/// Finalizes the handshake by sending the list of files to the receiver and
/// starting the file transfer task.
///
/// # Arguments
///
/// * `context` - The mutable context holding the sender, files, and shared key.
///
/// # Returns
///
/// A `Status` indicating the success or failure of the handshake finalization.
fn on_handshake_finalize(context: &mut Context) -> Status { fn on_handshake_finalize(context: &mut Context) -> Status {
// Create a vector of `Entry`s from the files in the context
let mut entries = vec![]; let mut entries = vec![];
for (index, file) in context.files.iter().enumerate() { for (index, file) in context.files.iter().enumerate() {
let entry = list_packet::Entry { let entry = list_packet::Entry {
// The index of the file in the context
index: index.try_into().unwrap(), index: index.try_into().unwrap(),
// The name of the file
name: file.name.clone(), name: file.name.clone(),
// The size of the file
size: file.size, size: file.size,
}; };
entries.push(entry); entries.push(entry);
} }
// Send the encrypted list packet to the receiver
context.sender.send_encrypted_packet( context.sender.send_encrypted_packet(
&context.shared_key, &context.shared_key,
DESTINATION, DESTINATION,
Value::List(ListPacket { entries }), Value::List(ListPacket { entries }),
); );
// Spawn the file transfer task and store it in the context
context.task = Some(tokio::spawn(on_chunk( context.task = Some(tokio::spawn(on_chunk(
context.sender.clone(), context.sender.clone(),
context.shared_key.clone(), context.shared_key.clone(),
@ -230,34 +416,73 @@ fn on_handshake_finalize(context: &mut Context) -> Status {
Status::Continue() Status::Continue()
} }
/// Handles the handshake response packet received from the receiver.
///
/// # Arguments
///
/// * `context` - The mutable context holding the sender, files, and shared key.
/// * `handshake_response` - The handshake response packet received from the receiver.
///
/// # Returns
///
/// A `Status` indicating the success or failure of the handshake.
fn on_handshake(context: &mut Context, handshake_response: HandshakeResponsePacket) -> Status { fn on_handshake(context: &mut Context, handshake_response: HandshakeResponsePacket) -> Status {
// Check if the handshake has already been performed
if context.shared_key.is_some() { if context.shared_key.is_some() {
return Status::Err("Already performed handshake.".into()); return Status::Err("Already performed handshake.".into());
} }
// Create a HMAC instance with the HMAC key
let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap(); let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap();
// Update the HMAC with the public key from the handshake response
mac.update(&handshake_response.public_key); mac.update(&handshake_response.public_key);
// Verify the signature in the handshake response
let verification = mac.verify_slice(&handshake_response.signature); let verification = mac.verify_slice(&handshake_response.signature);
if verification.is_err() { if verification.is_err() {
// Return an error if the signature is invalid
return Status::Err("Invalid signature from the receiver.".into()); return Status::Err("Invalid signature from the receiver.".into());
} }
// Convert the public key from bytes to a public key instance
let shared_public_key = PublicKey::from_sec1_bytes(&handshake_response.public_key).unwrap(); let shared_public_key = PublicKey::from_sec1_bytes(&handshake_response.public_key).unwrap();
// Derive the shared secret using Diffie-Hellman key exchange
let shared_secret = context.key.diffie_hellman(&shared_public_key); let shared_secret = context.key.diffie_hellman(&shared_public_key);
// Extract the raw secret bytes from the shared secret
let shared_secret = shared_secret.raw_secret_bytes(); let shared_secret = shared_secret.raw_secret_bytes();
// Take the first 16 bytes of the secret as the shared key
let shared_secret = &shared_secret[0..16]; let shared_secret = &shared_secret[0..16];
// Convert the shared secret bytes to a `Key<Aes128Gcm>` instance
let shared_key: &Key<Aes128Gcm> = shared_secret.into(); let shared_key: &Key<Aes128Gcm> = shared_secret.into();
// Create a new instance of `Aes128Gcm` with the shared key
let shared_key = <Aes128Gcm as aes_gcm::KeyInit>::new(shared_key); let shared_key = <Aes128Gcm as aes_gcm::KeyInit>::new(shared_key);
// Set the shared key in the context
context.shared_key = Some(shared_key); context.shared_key = Some(shared_key);
// Finalize the handshake by sending the list of files and starting the file transfer task
on_handshake_finalize(context) on_handshake_finalize(context)
} }
/// Handles the incoming message from the WebSocket.
///
/// # Arguments
///
/// * `context` - The mutable context holding the sender and shared key.
/// * `message` - The incoming WebSocket message.
/// * `relay` - The URL of the relay.
/// * `transfer_name` - The name of the transfer.
/// * `is_local` - Whether the transfer is local or not.
///
/// # Returns
///
/// A `Status` indicating the success or failure of handling the message.
fn on_message( fn on_message(
context: &mut Context, context: &mut Context,
message: WebSocketMessage, message: WebSocketMessage,
@ -266,25 +491,36 @@ fn on_message(
is_local: bool, is_local: bool,
) -> Status { ) -> Status {
match message.clone() { match message.clone() {
// Handle the text WebSocket message
WebSocketMessage::Text(text) => { WebSocketMessage::Text(text) => {
// Parse the JSON packet from the text message
let packet = match serde_json::from_str(&text) { let packet = match serde_json::from_str(&text) {
Ok(packet) => packet, Ok(packet) => packet,
Err(_) => { Err(_) => {
return Status::Continue(); return Status::Continue();
} }
}; };
// Call the corresponding handler based on the packet type
return match packet { return match packet {
// Handle the `Create` packet
JsonPacketResponse::Create { id } => { JsonPacketResponse::Create { id } => {
on_create_room(context, id, relay, transfer_name, is_local) on_create_room(context, id, relay, transfer_name, is_local)
} }
// Handle the `Join` packet
JsonPacketResponse::Join { size } => on_join_room(context, size), JsonPacketResponse::Join { size } => on_join_room(context, size),
// Handle the `Leave` packet
JsonPacketResponse::Leave { index } => on_leave_room(context, index), JsonPacketResponse::Leave { index } => on_leave_room(context, index),
// Handle the `Error` packet
JsonPacketResponse::Error { message } => on_error(message), JsonPacketResponse::Error { message } => on_error(message),
}; };
} }
// Handle the binary WebSocket message
WebSocketMessage::Binary(data) => { WebSocketMessage::Binary(data) => {
// Extract the encrypted data from the binary message
let data = data[1..].to_vec(); let data = data[1..].to_vec();
// Decrypt the data using the shared key if available
let data = if let Some(shared_key) = &context.shared_key { let data = if let Some(shared_key) = &context.shared_key {
let nonce = &data[..NONCE_SIZE]; let nonce = &data[..NONCE_SIZE];
let ciphertext = &data[NONCE_SIZE..]; let ciphertext = &data[NONCE_SIZE..];
@ -294,23 +530,41 @@ fn on_message(
data data
}; };
// Decode the packet from the decrypted data
let packet = Packet::decode(data.as_ref()).unwrap(); let packet = Packet::decode(data.as_ref()).unwrap();
let value = packet.value.unwrap(); let value = packet.value.unwrap();
// Call the corresponding handler based on the packet value
return match value { return match value {
// Handle the `HandshakeResponse` packet
Value::HandshakeResponse(handshake_response) => { Value::HandshakeResponse(handshake_response) => {
on_handshake(context, handshake_response) on_handshake(context, handshake_response)
} }
// Handle the `Progress` packet
Value::Progress(progress) => on_progress(context, progress), Value::Progress(progress) => on_progress(context, progress),
// Handle unexpected packets
_ => Status::Err(format!("Unexpected packet: {:?}", value)), _ => Status::Err(format!("Unexpected packet: {:?}", value)),
}; };
} }
// Handle other message types
_ => (), _ => (),
} }
// Return an error for unsupported message types
Status::Err("Invalid message type".into()) Status::Err("Invalid message type".into())
} }
/// Starts the sender process.
///
/// # Arguments
///
/// * `socket` - The WebSocket connection.
/// * `paths` - The paths to the files to be sent.
/// * `room_id` - The ID of the room to join.
/// * `relay` - The URL of the relay server.
/// * `transfer_name` - The name of the transfer.
/// * `is_local` - Whether the transfer is local or not.
#[allow(clippy::needless_doctest_main)]
pub async fn start( pub async fn start(
socket: Socket, socket: Socket,
paths: Vec<String>, paths: Vec<String>,
@ -319,9 +573,11 @@ pub async fn start(
transfer_name: String, transfer_name: String,
is_local: bool, is_local: bool,
) { ) {
// Prepare the files to be sent
let mut files = vec![]; let mut files = vec![];
for path in paths { for path in paths {
// Open the file
let handle = match fs::File::open(&path) { let handle = match fs::File::open(&path) {
Ok(handle) => handle, Ok(handle) => handle,
Err(error) => { Err(error) => {
@ -332,6 +588,7 @@ pub async fn start(
let metadata = handle.metadata().unwrap(); let metadata = handle.metadata().unwrap();
// Check if the path points to a file
if metadata.is_dir() { if metadata.is_dir() {
error!("Error: The path '{}' does not point to a file.", path); error!("Error: The path '{}' does not point to a file.", path);
return; return;
@ -341,6 +598,7 @@ pub async fn start(
let size = metadata.len(); let size = metadata.len();
// Check if the file is empty
if size == 0 { if size == 0 {
error!("Error: The file '{}' is empty and cannot be sent.", name); error!("Error: The file '{}' is empty and cannot be sent.", name);
return; return;
@ -353,15 +611,20 @@ pub async fn start(
}); });
} }
// Generate the HMAC key
let mut hmac = [0u8; 32]; let mut hmac = [0u8; 32];
OsRng.fill_bytes(&mut hmac); OsRng.fill_bytes(&mut hmac);
// Generate the encryption key
let key = EphemeralSecret::random(&mut OsRng); let key = EphemeralSecret::random(&mut OsRng);
// Create the flume channels
let (sender, receiver) = flume::bounded(1000); let (sender, receiver) = flume::bounded(1000);
// Split the WebSocket connection
let (outgoing, incoming) = socket.split(); let (outgoing, incoming) = socket.split();
// Create the context
let mut context = Context { let mut context = Context {
sender, sender,
key, key,
@ -375,10 +638,12 @@ pub async fn start(
debug!("Attempting to create room..."); debug!("Attempting to create room...");
debug!("With Room-ID: {:?}", room_id); debug!("With Room-ID: {:?}", room_id);
// Send the create room packet
context.sender.send_json_packet(JsonPacket::Create { context.sender.send_json_packet(JsonPacket::Create {
id: room_id.clone(), id: room_id.clone(),
}); });
// Handle the incoming WebSocket messages
let outgoing_handler = receiver.stream().map(Ok).forward(outgoing); let outgoing_handler = receiver.stream().map(Ok).forward(outgoing);
let incoming_handler = incoming.try_for_each(|message| { let incoming_handler = incoming.try_for_each(|message| {
@ -390,15 +655,17 @@ pub async fn start(
is_local, is_local,
) { ) {
Status::Exit() => { Status::Exit() => {
// TODO: Signal Exit to the server // Send the leave room packet
context.sender.send_json_packet(JsonPacket::Leave); context.sender.send_json_packet(JsonPacket::Leave);
println!("Transfer has completed."); println!("Transfer has completed.");
// Return an error
return future::err(Error::ConnectionClosed); return future::err(Error::ConnectionClosed);
} }
Status::Err(error) => { Status::Err(error) => {
error!("Error: {}", error); error!("Error: {}", error);
// Return an error
return future::err(Error::ConnectionClosed); return future::err(Error::ConnectionClosed);
} }
_ => {} _ => {}
@ -409,6 +676,7 @@ pub async fn start(
pin_mut!(incoming_handler, outgoing_handler); pin_mut!(incoming_handler, outgoing_handler);
// Wait for the incoming or outgoing handlers to complete
future::select(incoming_handler, outgoing_handler).await; future::select(incoming_handler, outgoing_handler).await;
} }

View file

@ -8,18 +8,34 @@ use crate::relay::transfer::{TransferRequest, TransferResponse};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
/// Asynchronously sends information about the sender to the specified relay.
///
/// # Arguments
///
/// * `relay` - The URL of the relay.
/// * `name` - The name of the sender.
/// * `room_id` - The ID of the room.
/// * `is_local` - Indicates whether the sender is local.
///
/// # Returns
///
/// A `Result` containing a `TransferResponse` if the request was successful, or an error if it failed.
pub async fn send_info( pub async fn send_info(
relay: &str, relay: &str,
name: &str, name: &str,
room_id: &str, room_id: &str,
is_local: bool, is_local: bool,
) -> Result<TransferResponse> { ) -> Result<TransferResponse> {
// Build the URL for the request
let url = relay.to_string(); let url = relay.to_string();
// Get the sender's IP address
let sender_ip = match local_ipv6() { let sender_ip = match local_ipv6() {
Ok(ip) => ip, Ok(ip) => ip,
Err(_) => match local_ip() { Err(_) => match local_ip() {
Ok(ip) => ip, Ok(ip) => ip,
Err(e) => { Err(e) => {
// Log the error and return the error
error!("Error getting local ip: {e:?}"); error!("Error getting local ip: {e:?}");
return Err(Box::new(e)); return Err(Box::new(e));
} }
@ -27,14 +43,19 @@ pub async fn send_info(
}; };
let ip_str = sender_ip.to_owned().to_string(); let ip_str = sender_ip.to_owned().to_string();
// Create the transfer request
let transfer_request = TransferRequest { let transfer_request = TransferRequest {
// Set the name of the sender
name: String::from(name), name: String::from(name),
// Set the IP address of the sender
ip: ip_str, ip: ip_str,
// Set the room ID for the local sender
local_room_id: if is_local { local_room_id: if is_local {
String::from(room_id) String::from(room_id)
} else { } else {
String::from("") String::from("")
}, },
// Set the room ID for the relay sender
relay_room_id: if !is_local { relay_room_id: if !is_local {
String::from(room_id) String::from(room_id)
} else { } else {
@ -42,7 +63,10 @@ pub async fn send_info(
}, },
}; };
// Log the start of the request
debug!("Trying to send Request."); debug!("Trying to send Request.");
// Send the request and parse the response
let result: Result<TransferResponse> = task::spawn_blocking(move || { let result: Result<TransferResponse> = task::spawn_blocking(move || {
let client = Client::new(); let client = Client::new();
let response = client let response = client
@ -54,5 +78,6 @@ pub async fn send_info(
}) })
.await?; .await?;
// Return the result
result result
} }

View file

@ -18,20 +18,33 @@ use tower_http::trace::{DefaultMakeSpan, TraceLayer};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use uuid::Uuid; use uuid::Uuid;
/// Start the sender process.
///
/// This function starts the sender process which connects to a relay and
/// initiates file transfers. The sender also starts a local WebSocket server.
///
/// # Arguments
///
/// * `name` - The name of the sender.
/// * `relay` - The relay to connect to.
/// * `files` - The files to transfer.
pub async fn start_sender(name: String, relay: Arc<String>, files: Arc<Vec<String>>) { pub async fn start_sender(name: String, relay: Arc<String>, files: Arc<Vec<String>>) {
// Log the name of the sender
debug!("Got name: {:?}", name); debug!("Got name: {:?}", name);
// Create a channel for communication between threads
let (tx, mut rx) = mpsc::channel(1); let (tx, mut rx) = mpsc::channel(1);
debug!("Got relay: {relay}"); // Generate a unique room ID
let room_id = Uuid::new_v4().to_string(); let room_id = Uuid::new_v4().to_string();
// let rand_name = generate_random_name();
let local_room_id = room_id.clone(); let local_room_id = room_id.clone();
let local_files = files.clone(); let local_files = files.clone();
let local_relay = relay.clone(); let local_relay = relay.clone();
let local_rand_name = name.clone(); let local_rand_name = name.clone();
let local_tx = tx.clone(); let local_tx = tx.clone();
// Start a local WebSocket server
let local_ws_thread = task::spawn(async move { let local_ws_thread = task::spawn(async move {
start_local_ws().await; start_local_ws().await;
}); });
// Connect to the relay
let relay_thread = task::spawn(async move { let relay_thread = task::spawn(async move {
connect_to_server( connect_to_server(
relay.clone(), relay.clone(),
@ -44,6 +57,7 @@ pub async fn start_sender(name: String, relay: Arc<String>, files: Arc<Vec<Strin
) )
.await .await
}); });
// Connect to the local WebSocket server
let local_thread = task::spawn(async move { let local_thread = task::spawn(async move {
connect_to_server( connect_to_server(
Arc::new(String::from("ws://0.0.0.0:9000")), Arc::new(String::from("ws://0.0.0.0:9000")),
@ -57,18 +71,40 @@ pub async fn start_sender(name: String, relay: Arc<String>, files: Arc<Vec<Strin
.await .await
}); });
// Wait for the sender threads to finish
rx.recv().await.unwrap(); rx.recv().await.unwrap();
// Abort the local WebSocket server thread
local_ws_thread.abort(); local_ws_thread.abort();
// Abort the relay thread
relay_thread.abort(); relay_thread.abort();
// Abort the local thread
local_thread.abort(); local_thread.abort();
} }
/// Start a local WebSocket server.
///
/// This function initializes and runs a WebSocket server on the specified host and port.
/// It creates an instance of the `AppState` struct and uses it as the state for the router.
/// The `ws_handler` function is registered as the handler for the "/ws" route.
///
/// # Arguments
///
/// None
///
/// # Returns
///
/// This function does not return anything.
pub async fn start_local_ws() { pub async fn start_local_ws() {
// The host and port the server will listen on.
let app_host = "0.0.0.0"; let app_host = "0.0.0.0";
let app_port = "9000"; let app_port = "9000";
// Create an instance of the application state.
let server = AppState::new(); let server = AppState::new();
// Create the axum application.
// The `ws_handler` function is registered as the handler for the "/ws" route.
// The `AppState` instance is used as the state for the router.
let app = Router::new() let app = Router::new()
.route("/ws", get(ws_handler)) .route("/ws", get(ws_handler))
.with_state(server) .with_state(server)
@ -77,12 +113,16 @@ pub async fn start_local_ws() {
.make_span_with(DefaultMakeSpan::default().include_headers(true)), .make_span_with(DefaultMakeSpan::default().include_headers(true)),
); );
// Try to bind the server to the specified host and port.
if let Ok(listener) = TcpListener::bind(&format!("{}:{}", app_host, app_port)).await { if let Ok(listener) = TcpListener::bind(&format!("{}:{}", app_host, app_port)).await {
// Log the address the server is listening on.
info!( info!(
"Local Websocket listening on: {}", "Local WebSocket listening on: {}",
listener.local_addr().unwrap() listener.local_addr().unwrap()
); );
// Serve the application using the listener.
// The `connect_info` parameter is used to include the client's socket address in the tracing spans.
axum::serve( axum::serve(
listener, listener,
app.into_make_service_with_connect_info::<SocketAddr>(), app.into_make_service_with_connect_info::<SocketAddr>(),
@ -90,10 +130,22 @@ pub async fn start_local_ws() {
.await .await
.unwrap(); .unwrap();
} else { } else {
// Log an error if the server fails to bind.
error!("Failed to listen on: {}:{}", app_host, app_port); error!("Failed to listen on: {}:{}", app_host, app_port);
} }
} }
/// Connects to the specified server and starts the file transfer.
///
/// # Arguments
///
/// * `relay` - The relay server URL.
/// * `files` - The files to be transferred.
/// * `room_id` - The room ID for the transfer. If `None`, a random UUID is generated.
/// * `message_server` - The message server URL.
/// * `transfer_name` - The name of the transfer.
/// * `tx` - The sender end of a channel to signal the completion of the transfer.
/// * `is_local` - Whether the transfer is local or not.
async fn connect_to_server( async fn connect_to_server(
relay: Arc<String>, relay: Arc<String>,
files: Arc<Vec<String>>, files: Arc<Vec<String>>,
@ -103,21 +155,33 @@ async fn connect_to_server(
tx: mpsc::Sender<()>, tx: mpsc::Sender<()>,
is_local: bool, is_local: bool,
) { ) {
// Construct the server URL.
let url = format!("{}/ws", relay); let url = format!("{}/ws", relay);
// Construct the message server URL.
let message_relay = format!("{}", message_server); let message_relay = format!("{}", message_server);
// Construct the transfer name.
let transfer_name = format!("{}", transfer_name); let transfer_name = format!("{}", transfer_name);
// Create a request to the server.
match url.clone().into_client_request() { match url.clone().into_client_request() {
Ok(mut request) => { Ok(mut request) => {
// Set the "Origin" header.
request request
.headers_mut() .headers_mut()
.insert("Origin", HeaderValue::from_str(relay.as_ref()).unwrap()); .insert("Origin", HeaderValue::from_str(relay.as_ref()).unwrap());
// Log the connection attempt.
debug!("Attempting to connect to {url}..."); debug!("Attempting to connect to {url}...");
// Generate a room ID if not provided.
let room_id = match room_id { let room_id = match room_id {
Some(id) => id, Some(id) => id,
None => Uuid::new_v4().to_string(), None => Uuid::new_v4().to_string(),
}; };
// Connect to the server and start the file transfer.
match connect_async(request).await { match connect_async(request).await {
Ok((socket, _)) => { Ok((socket, _)) => {
let paths = files.to_vec(); let paths = files.to_vec();
@ -130,14 +194,18 @@ async fn connect_to_server(
is_local, is_local,
) )
.await; .await;
// Signal the completion of the transfer.
tx.send(()).await.unwrap(); tx.send(()).await.unwrap();
} }
Err(e) => { Err(e) => {
// Log the connection error.
error!("Error: Failed to connect with error: {e}"); error!("Error: Failed to connect with error: {e}");
} }
} }
} }
Err(e) => { Err(e) => {
// Log the request creation error.
error!("Error: failed to create request with reason: {e:?}"); error!("Error: failed to create request with reason: {e:?}");
} }
} }

View file

@ -2,40 +2,80 @@ use hex;
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
/// Generates a random name composed of an adjective, a noun, and another noun.
///
/// # Returns
///
/// A string in the format "{adjective}-{noun1}-{noun2}".
pub fn generate_random_name() -> String { pub fn generate_random_name() -> String {
let mut rng = thread_rng(); let mut rng = thread_rng();
let adjective = adjectives().choose(&mut rng).unwrap(); let adjective = adjectives().choose(&mut rng).unwrap();
// let adjective = adjectives().sample(&mut rng).unwrap();
let noun1 = nouns1().choose(&mut rng).unwrap(); let noun1 = nouns1().choose(&mut rng).unwrap();
let noun2 = nouns2().choose(&mut rng).unwrap(); let noun2 = nouns2().choose(&mut rng).unwrap();
format!("{adjective}-{noun1}-{noun2}") format!("{adjective}-{noun1}-{noun2}")
} }
/// Returns a random adjective.
///
/// # Returns
///
/// A `&'static str` representing an adjective.
fn adjectives() -> &'static [&'static str] { fn adjectives() -> &'static [&'static str] {
// Define a list of adjectives.
static ADJECTIVES: &[&str] = &["funny", "smart", "creative", "friendly", "great"]; static ADJECTIVES: &[&str] = &["funny", "smart", "creative", "friendly", "great"];
ADJECTIVES ADJECTIVES
} }
/// Returns a random noun.
///
/// # Returns
///
/// A `&'static str` representing a noun.
fn nouns1() -> &'static [&'static str] { fn nouns1() -> &'static [&'static str] {
// Define a list of nouns.
static NOUNS1: &[&str] = &["dog", "cat", "flower", "tree", "house"]; static NOUNS1: &[&str] = &["dog", "cat", "flower", "tree", "house"];
NOUNS1 NOUNS1
} }
/// Returns a random noun.
///
/// # Returns
///
/// A `&'static str` representing a noun.
fn nouns2() -> &'static [&'static str] { fn nouns2() -> &'static [&'static str] {
// Define a list of nouns.
static NOUNS2: &[&str] = &["cookie", "cake", "frosting"]; static NOUNS2: &[&str] = &["cookie", "cake", "frosting"];
NOUNS2 NOUNS2
} }
/// Hashes a given name using SHA256 and returns the hex-encoded result.
///
/// # Parameters
///
/// * `name`: A `String` representing the name to be hashed.
///
/// # Returns
///
/// A `String` containing the hex-encoded hash of the name.
pub fn hash_random_name(name: String) -> String { pub fn hash_random_name(name: String) -> String {
let hashed_name = Sha256::digest(name.as_bytes()); let hashed_name = Sha256::digest(name.as_bytes());
hex::encode(hashed_name) hex::encode(hashed_name)
} }
/// Replaces occurrences of "ws://" and "wss://" in a given address with "http://" and "https://" respectively.
///
/// # Parameters
///
/// * `address`: A `&str` representing the address to modify.
///
/// # Returns
///
/// A `String` representing the modified address.
pub fn replace_protocol(address: &str) -> String { pub fn replace_protocol(address: &str) -> String {
let mut result = address.to_string(); let mut result = address.to_string();
result = result.replace("ws://", "http://"); result = result.replace("ws://", "http://");
result = result.replace("wss://", "https://"); result = result.replace("wss://", "https://");
result result

View file

@ -14,46 +14,116 @@ use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::protocol::Message as WebSocketMessage; use tokio_tungstenite::tungstenite::protocol::Message as WebSocketMessage;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
/// Represents a packet that is sent over a websocket connection.
///
/// This enum is used to represent different types of packets that can be sent over a websocket connection.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum JsonPacket { pub enum JsonPacket {
Join { id: String }, /// A packet to join a room.
Create { id: Option<String> }, ///
/// This variant is used to request to join a room. The `id` field is used to specify the room id.
Join {
/// The id of the room to join.
id: String,
},
/// A packet to create a new room.
///
/// This variant is used to request to create a new room. The `id` field is used to specify the room id, which can be optional.
Create {
/// The id of the room to create. It can be `None` to generate a random room id.
id: Option<String>,
},
/// A packet to leave a room.
///
/// This variant is used to request to leave a room.
Leave, Leave,
} }
/// Represents a response to a `JsonPacket` packet.
///
/// This enum is used to represent different types of responses to a `JsonPacket` packet.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum JsonPacketResponse { pub enum JsonPacketResponse {
/// A response to a `Join` packet.
///
/// This variant is used to indicate the result of a `Join` packet. The `size` field is used to specify the number of existing users in the room.
Join { Join {
/// The number of existing users in the room. This field is `None` if the room is empty.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
size: Option<usize>, size: Option<usize>,
}, },
/// A response to a `Create` packet.
///
/// This variant is used to indicate the result of a `Create` packet. The `id` field is used to specify the room id.
Create { Create {
/// The id of the created room.
id: String, id: String,
}, },
/// A response to a `Leave` packet.
///
/// This variant is used to indicate the result of a `Leave` packet. The `index` field is used to specify the index of the user who left the room.
Leave { Leave {
/// The index of the user who left the room.
index: usize, index: usize,
}, },
/// An error response.
///
/// This variant is used to indicate an error. The `message` field is used to specify the error message.
Error { Error {
/// The error message.
message: String, message: String,
}, },
} }
/// Represents the result of an operation.
///
/// This enum is used to indicate the status of an operation. It can be one of three
/// variants:
///
/// - `Continue`: Operation was successful and the client should continue.
/// - `Exit`: Operation was successful and the client should exit.
/// - `Err`: Operation encountered an error. The error message is provided in the
/// variant.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Status { pub enum Status {
/// Operation was successful and the client should continue.
Continue(), Continue(),
/// Operation was successful and the client should exit.
Exit(), Exit(),
/// Operation encountered an error. The error message is provided in the variant.
Err(String), Err(String),
} }
/// Represents a sender of JSON packets.
///
/// This trait is used to send JSON packets to a `JsonPacket` receiver. The
/// `send_json_packet` method is used to send a `JsonPacket` packet.
pub trait JsonPacketSender { pub trait JsonPacketSender {
/// Sends a `JsonPacket` packet to a receiver.
///
/// This method sends a `JsonPacket` packet to a receiver. The `packet` argument
/// is the packet to send.
fn send_json_packet(&self, packet: JsonPacket); fn send_json_packet(&self, packet: JsonPacket);
} }
/// Represents a sender of packets.
///
/// This trait is used to send packets to a receiver. The `send_packet` method is used to send
/// a plain packet, and the `send_encrypted_packet` method is used to send an encrypted packet.
pub trait PacketSender { pub trait PacketSender {
/// Sends a plain packet to a receiver.
///
/// This method sends a plain packet to a receiver. The `destination` argument specifies the
/// destination of the packet, and the `packet` argument is the packet to send.
fn send_packet(&self, destination: u8, packet: packets::packet::Value); fn send_packet(&self, destination: u8, packet: packets::packet::Value);
/// Sends an encrypted packet to a receiver.
///
/// This method sends an encrypted packet to a receiver. The `key` argument is the encryption
/// key to use, the `destination` argument specifies the destination of the packet, and the
/// `value` argument is the packet to send.
fn send_encrypted_packet( fn send_encrypted_packet(
&self, &self,
key: &Option<Aes128Gcm>, key: &Option<Aes128Gcm>,
@ -62,47 +132,97 @@ pub trait PacketSender {
); );
} }
impl JsonPacketSender for Sender {
fn send_json_packet(&self, packet: JsonPacket) {
let serialized_packet =
serde_json::to_string(&packet).expect("Failed to serialize JSON packet.");
/// Implementation of `JsonPacketSender` for `Sender` struct.
///
/// This implementation of `JsonPacketSender` for `Sender` struct provides a method
/// `send_json_packet` to send a `JsonPacket` packet.
impl JsonPacketSender for Sender {
/// Sends a `JsonPacket` packet to a receiver.
///
/// This method serializes the `JsonPacket` using `serde_json` and sends it as a
/// `WebSocketMessage::Text` to a receiver.
///
/// # Arguments
///
/// * `packet` - The `JsonPacket` to send.
fn send_json_packet(&self, packet: JsonPacket) {
// Serialize the JsonPacket using serde_json
let serialized_packet = serde_json::to_string(&packet)
.expect("Failed to serialize JSON packet.");
// Send the serialized packet as a WebSocketMessage::Text
self.send(WebSocketMessage::Text(serialized_packet)) self.send(WebSocketMessage::Text(serialized_packet))
.expect("Failed to send JSON packet."); .expect("Failed to send JSON packet.");
} }
} }
/// Implementation of `PacketSender` for `Sender` struct.
///
/// This implementation of `PacketSender` for `Sender` struct provides methods
/// to send a packet to a receiver.
impl PacketSender for Sender { impl PacketSender for Sender {
/// Sends a packet to a receiver.
///
/// This method serializes the packet and sends it as a `WebSocketMessage::Binary` to a receiver.
///
/// # Arguments
///
/// * `destination` - The destination of the packet.
/// * `value` - The packet to send.
fn send_packet(&self, destination: u8, value: packets::packet::Value) { fn send_packet(&self, destination: u8, value: packets::packet::Value) {
// Serialize the packet
let packet = Packet { value: Some(value) }; let packet = Packet { value: Some(value) };
let mut serialized_packet = packet.encode_to_vec(); let mut serialized_packet = packet.encode_to_vec();
// Insert the destination at the beginning of the packet
serialized_packet.insert(0, destination); serialized_packet.insert(0, destination);
// Send the serialized packet as a WebSocketMessage::Binary
self.send(WebSocketMessage::Binary(serialized_packet)) self.send(WebSocketMessage::Binary(serialized_packet))
.expect("Failed to send Packet."); .expect("Failed to send Packet.");
} }
/// Sends an encrypted packet to a receiver.
///
/// This method encrypts the packet using the provided key and sends it as a
/// `WebSocketMessage::Binary` to a receiver.
///
/// # Arguments
///
/// * `key` - The encryption key to use.
/// * `destination` - The destination of the packet.
/// * `value` - The packet to send.
fn send_encrypted_packet( fn send_encrypted_packet(
&self, &self,
key: &Option<Aes128Gcm>, key: &Option<Aes128Gcm>,
destination: u8, destination: u8,
value: packets::packet::Value, value: packets::packet::Value,
) { ) {
// Serialize the packet
let packet = Packet { value: Some(value) }; let packet = Packet { value: Some(value) };
// Generate a nonce for encryption
let nonce = Aes128Gcm::generate_nonce(&mut OsRng); let nonce = Aes128Gcm::generate_nonce(&mut OsRng);
// Serialize the packet
let plaintext = packet.encode_to_vec(); let plaintext = packet.encode_to_vec();
// Encrypt the packet using the provided key
let mut ciphertext = key let mut ciphertext = key
.as_ref() .as_ref()
.unwrap() .unwrap()
.encrypt(&nonce, plaintext.as_ref()) .encrypt(&nonce, plaintext.as_ref())
.expect("Failed to encrypt Packet."); .expect("Failed to encrypt Packet.");
// Create the serialized packet by concatenating the nonce and the ciphertext
let mut serialized_packet = nonce.to_vec(); let mut serialized_packet = nonce.to_vec();
serialized_packet.append(&mut ciphertext); serialized_packet.append(&mut ciphertext);
// Insert the destination at the beginning of the packet
serialized_packet.insert(0, destination); serialized_packet.insert(0, destination);
// Send the serialized packet as a WebSocketMessage::Binary
self.send(WebSocketMessage::Binary(serialized_packet)) self.send(WebSocketMessage::Binary(serialized_packet))
.expect("Failed to send encrypted Packet."); .expect("Failed to send encrypted Packet.");
} }

View file

@ -10,6 +10,18 @@ use caesar_core::relay::server::upload_info;
use caesar_core::relay::server::ws_handler; use caesar_core::relay::server::ws_handler;
use shuttle_axum::ShuttleAxum; use shuttle_axum::ShuttleAxum;
/// The main function that sets up the Axum application.
///
/// This function creates a new server data structure and sets up the application routes.
/// The routes include "/ws" for the websocket handler, "/upload" for the upload info handler,
/// "/download/:name" for the download info handler, and "/download_success/:name" for the download success handler.
/// The routes are associated with the corresponding handlers.
///
/// The application state is wrapped around the routes using the `with_state` method.
/// The client IP source is added as an extension using the `layer` method.
///
/// The function returns a `ShuttleAxum` result.
#[shuttle_runtime::main] #[shuttle_runtime::main]
async fn axum() -> ShuttleAxum { async fn axum() -> ShuttleAxum {
// Create a new server data structure. // Create a new server data structure.
@ -17,12 +29,13 @@ async fn axum() -> ShuttleAxum {
// Set up the application routes. // Set up the application routes.
let app = Router::new() let app = Router::new()
.route("/ws", get(ws_handler)) .route("/ws", get(ws_handler)) // Route for the websocket handler
.route("/upload", put(upload_info)) .route("/upload", put(upload_info)) // Route for the upload info handler
.route("/download/:name", get(download_info)) .route("/download/:name", get(download_info)) // Route for the download info handler
.route("/download_success/:name", post(download_success)) .route("/download_success/:name", post(download_success)) // Route for the download success handler
.with_state(appstate) .with_state(appstate) // Wrap the routes with the application state
.layer(SecureClientIpSource::ConnectInfo.into_extension()); .layer(SecureClientIpSource::ConnectInfo.into_extension()); // Add the client IP source as an extension
// Return the application router wrapped in a `ShuttleAxum` result.
Ok(app.into()) Ok(app.into())
} }

View file

@ -8,92 +8,161 @@ import 'package:flutter_test_gui/pages/send_screen.dart';
import 'package:flutter_test_gui/pages/receive_screen.dart'; import 'package:flutter_test_gui/pages/receive_screen.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
/// Main entrypoint of the application.
///
/// This function is called when the application starts. It initializes the
/// Rust library, sets up the application widget, and shows the window.
///
/// The function first calls the [RustLib.init] function to initialize the
/// Rust library. Then, it runs the application using the [runApp] function
/// with the [MyApp] widget. If the application is running on Windows, Linux,
/// or macOS, it sets up the window properties such as the minimum size,
/// initial size, alignment, and title. Finally, it shows the window.
Future<void> main() async { Future<void> main() async {
// Initialize the Rust library
await RustLib.init(); await RustLib.init();
// Set up the application widget
runApp(const MyApp()); runApp(const MyApp());
// Set up the window properties if running on Windows, Linux, or macOS
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
doWhenWindowReady(() { doWhenWindowReady(() {
final win = appWindow; final win = appWindow;
// Set the minimum size of the window
const initialSize = Size(720, 512); const initialSize = Size(720, 512);
win.minSize = initialSize; win.minSize = initialSize;
// Set the initial size of the window
win.size = initialSize; win.size = initialSize;
// Set the alignment of the window
win.alignment = Alignment.center; win.alignment = Alignment.center;
// Set the title of the window
win.title = 'Caesar Test Demo'; win.title = 'Caesar Test Demo';
// Show the window
win.show(); win.show();
}); });
} }
} }
// const backColor = Color(0xFF32363E); /// The root widget of the application.
// const highlightColor = Color(0xFF98C379); ///
// const textColor = Color(0xFFABB2BF); /// It sets up the material design theme and provides the home page.
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
/// Creates a new instance of [MyApp].
const MyApp({super.key}); const MyApp({super.key});
@override @override
State<MyApp> createState() => _MyAppState(); State<MyApp> createState() => _MyAppState();
} }
/// The state for the [MyApp] widget.
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Set up the material design theme.
return MaterialApp( return MaterialApp(
title: 'Caesar-Transfer', title: 'Caesar-Transfer',
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
), ),
home: const MyHomePage(title: 'Caesar-Transfer'), // Set the home page.
home: const MyHomePage(
title: 'Caesar-Transfer',
),
); );
} }
} }
/// The root widget of the application that represents the home page.
///
/// It sets up the material design theme and provides the home page.
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title}); /// Creates a new instance of [MyHomePage].
///
/// The [title] argument is the title of the home page.
const MyHomePage({
super.key,
required this.title,
});
/// The title of the home page.
final String title; final String title;
@override @override
State<MyHomePage> createState() => _MyHomePageState(); State<MyHomePage> createState() => _MyHomePageState();
} }
/// The state for the [MyHomePage] widget.
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
/// The list of screens that can be displayed on the home page.
final List<Widget> _screens = [ final List<Widget> _screens = [
SendScreen(), SendScreen(),
ReceiveScreen(), ReceiveScreen(),
]; ];
/// The index of the currently selected screen.
int _selectedIndex = 0; int _selectedIndex = 0;
/// Handles the tap event on a tab.
///
/// Updates the selected index and rebuilds the widget tree.
///
/// The [index] argument is the index of the selected tab.
void _onItemTapped(int index) { void _onItemTapped(int index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
}); });
} }
/// Builds the user interface for the home page.
///
/// It creates a [MaterialApp] widget with a [Scaffold] as the home page.
/// The [Scaffold] includes an [AppBar], [BottomNavigationBar], and a [Body].
/// The [AppBar] displays the title of the app and a [PopupMenuButton] for
/// accessing the settings screen. The [BottomNavigationBar] displays icons
/// for the send and receive screens and allows the user to select one of them.
/// The [Body] displays the currently selected screen.
///
/// Returns a [Widget] that represents the user interface for the home page.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: Scaffold( home: Scaffold(
// Sets the background color of the scaffold.
backgroundColor: Constants.backColor, backgroundColor: Constants.backColor,
appBar: AppBar( appBar: AppBar(
// Sets the background color of the app bar.
backgroundColor: const Color(0xFF292c3c), //0xFF282C34), backgroundColor: const Color(0xFF292c3c), //0xFF282C34),
centerTitle: true, centerTitle: true,
// Sets the title of the app bar.
title: Text( title: Text(
widget.title, widget.title,
// Sets the style of the title text.
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
// Sets the action buttons for the app bar.
actions: [ actions: [
PopupMenuButton<String>( PopupMenuButton<String>(
// Sets the action to perform when a menu item is selected.
onSelected: (String result) { onSelected: (String result) {
if (result == 'Settings') { if (result == 'Settings') {
// Navigates to the settings screen when the 'Settings' menu item is selected.
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => SettingsScreen()), MaterialPageRoute(builder: (context) => SettingsScreen()),
); );
} }
}, },
// Sets the items to display in the popup menu.
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[ itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>( const PopupMenuItem<String>(
// Sets the value and label of a menu item.
value: 'Settings', value: 'Settings',
child: Text('Settings'), child: Text('Settings'),
) )
@ -101,11 +170,16 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
], ],
), ),
// Sets the body of the scaffold.
body: _screens[_selectedIndex], body: _screens[_selectedIndex],
// Sets the bottom navigation bar.
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
// Sets the background color of the bottom navigation bar.
backgroundColor: const Color(0xFF292c3c), //0xFF282C34), backgroundColor: const Color(0xFF292c3c), //0xFF282C34),
// Sets the items to display in the bottom navigation bar.
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
// Sets the icon and label of a bottom navigation bar item.
icon: Icon(Icons.send), icon: Icon(Icons.send),
label: 'Send', label: 'Send',
), ),
@ -114,9 +188,11 @@ class _MyHomePageState extends State<MyHomePage> {
label: 'Receive', label: 'Receive',
), ),
], ],
// Sets the currently selected item in the bottom navigation bar.
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
selectedItemColor: Constants.highlightColor, selectedItemColor: Constants.highlightColor,
unselectedItemColor: Constants.textColor, unselectedItemColor: Constants.textColor,
// Sets the action to perform when an item is tapped.
onTap: _onItemTapped, onTap: _onItemTapped,
), ),
), ),

View file

@ -11,28 +11,52 @@ import 'package:shared_preferences/shared_preferences.dart';
// import 'package:flutter_test_gui/src/rust/frb_generated.dart'; // import 'package:flutter_test_gui/src/rust/frb_generated.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
/// Screen for receiving files.
///
/// This screen is used to accept incoming file transfers. It displays a QR
/// code scanner on supported platforms and allows the user to enter a
/// connection link manually.
class ReceiveScreen extends StatefulWidget { class ReceiveScreen extends StatefulWidget {
/// Creates a new instance of the receive screen.
const ReceiveScreen({super.key}); const ReceiveScreen({super.key});
@override @override
ReceiveScreenState createState() => ReceiveScreenState(); ReceiveScreenState createState() => ReceiveScreenState();
} }
/// State for the receive screen.
class ReceiveScreenState extends State<ReceiveScreen> { class ReceiveScreenState extends State<ReceiveScreen> {
/// The URL of the app that initiated the transfer.
String appOrigin = ''; String appOrigin = '';
/// Text editing controller for the connection link input.
final myController = TextEditingController(); final myController = TextEditingController();
/// The current input value of the connection link input.
String inputValue = ''; String inputValue = '';
/// Whether to show the QR code scanner.
bool _showScanner = false; bool _showScanner = false;
/// Builds the QR code scanner widget.
///
/// If the platform is iOS or Android, a QR code scanner is displayed. If the
/// platform is not supported, an empty box is returned.
///
/// Returns a QR code scanner widget if the platform is supported, otherwise an
/// empty box.
Widget _buildQRScanner() { Widget _buildQRScanner() {
// Check if the platform is iOS or Android
if (Platform.isIOS || Platform.isAndroid) { if (Platform.isIOS || Platform.isAndroid) {
return MobileScanner( return MobileScanner(
controller: MobileScannerController( controller: MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates), detectionSpeed: DetectionSpeed.noDuplicates),
onDetect: (barcode) { onDetect: (barcode) {
// Check if the scanner failed to scan a QR code
if (barcode.raw == null) { if (barcode.raw == null) {
debugPrint('Failed to scan qr code'); debugPrint('Failed to scan qr code');
} else { } else {
// Set the input value to the scanned code
final String code = barcode.barcodes.first.displayValue.toString(); final String code = barcode.barcodes.first.displayValue.toString();
print(code); print(code);
setState(() { setState(() {
@ -43,25 +67,49 @@ class ReceiveScreenState extends State<ReceiveScreen> {
}, },
); );
} else { } else {
// If the platform is not supported, hide the scanner
_showScanner = false; _showScanner = false;
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
} }
/// Loads the app origin from the shared preferences.
///
/// If the app origin is not present in the shared preferences, it sets the
/// default value to 'wss://caesar-transfer-iu.shuttleapp.rs'.
///
/// Returns a [Future] that completes with no value.
Future<void> loadSettings() async { Future<void> loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
appOrigin = prefs.getString('app_origin') ?? appOrigin = prefs.getString('app_origin') ??
'wss://caesar-transfer-iu.shuttleapp.rs'; // Laden Sie die app_origin 'wss://caesar-transfer-iu.shuttleapp.rs'; // Load the app origin
} }
/// Requests permission for a given [permission].
///
/// If the permission is already granted, it returns true. If the permission
/// is not granted, it requests the permission and returns true if the user
/// grants the permission, otherwise it returns false.
///
/// Returns a [Future] that completes with a [bool] value indicating whether
/// the permission was granted or not.
Future<bool> _requestPermission(Permission permission) async { Future<bool> _requestPermission(Permission permission) async {
// Print the function name
print("In _requestPermission"); print("In _requestPermission");
// Check if the permission is already granted
if (await permission.isGranted) { if (await permission.isGranted) {
// Print the message
print("Granted"); print("Granted");
return true; return true;
} else { } else {
// Print the message
print("Else Zweig"); print("Else Zweig");
// Request the permission
final result = await permission.request(); final result = await permission.request();
// Check if the permission is granted
if (result == PermissionStatus.granted) { if (result == PermissionStatus.granted) {
return true; return true;
} else { } else {
@ -70,25 +118,49 @@ class ReceiveScreenState extends State<ReceiveScreen> {
} }
} }
/// Starts a transfer by getting the directory path from the user and navigating
/// to the [TransferScreen] with the given [input] and [filePath].
///
/// The [input] is the value of the input field. If it is not empty, it gets the
/// directory path from the user using the [FilePicker.platform.getDirectoryPath]
/// method. If the user chooses a directory, it sets the [filePath] to the
/// selected directory path. If the user doesn't choose a directory, it prints
/// a message indicating that the user didn't choose a directory.
///
/// If the platform is Android, it checks if the external storage permission is
/// granted. If it is not granted, it requests the permission and navigates to
/// the [TransferScreen] with the given [input] and [filePath]. If the permission
/// is granted, it navigates to the [MyHomePage] with the title 'Caesar Transfer'.
///
/// If the platform is not Android, it navigates to the [TransferScreen] with the
/// given [input] and [filePath].
///
/// Returns a [Future] that completes with no value.
Future<void> _startTransfer(String appOrigin) async { Future<void> _startTransfer(String appOrigin) async {
final input = inputValue.trim(); final input = inputValue.trim();
String filePath = ''; String filePath = '';
if (input.isNotEmpty) { if (input.isNotEmpty) {
// Get the directory path from the user
String? selectDirectory = await FilePicker.platform.getDirectoryPath(); String? selectDirectory = await FilePicker.platform.getDirectoryPath();
if (selectDirectory == null) { if (selectDirectory == null) {
print("User doesnt choose a directory"); // Print a message indicating that the user didn't choose a directory
print("User doesn't choose a directory");
} else { } else {
print("user choose: $selectDirectory"); // Set the filePath to the selected directory path
print("User chose: $selectDirectory");
filePath = selectDirectory; filePath = selectDirectory;
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
// Check if the external storage permission is granted
if (await _requestPermission(Permission.manageExternalStorage)) { if (await _requestPermission(Permission.manageExternalStorage)) {
// Navigate to the TransferScreen
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TransferScreen( builder: (context) => TransferScreen(
transferName: input, directory: filePath))); transferName: input, directory: filePath)));
} else { } else {
// Navigate to the MyHomePage with the title 'Caesar Transfer'
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -96,6 +168,7 @@ class ReceiveScreenState extends State<ReceiveScreen> {
const MyHomePage(title: 'Caesar Transfer'))); const MyHomePage(title: 'Caesar Transfer')));
} }
} else { } else {
// Navigate to the TransferScreen
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -105,6 +178,12 @@ class ReceiveScreenState extends State<ReceiveScreen> {
} }
} }
/// Builds the scaffold for the receive screen.
///
/// The scaffold contains a center widget that contains a column of widgets.
/// The column contains a QR code scanner if [_showScanner] is true, otherwise
/// it contains a text field for entering the transfer name. Below the text
/// field is an elevated button for initiating the receive process.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -113,6 +192,8 @@ class ReceiveScreenState extends State<ReceiveScreen> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// If _showScanner is false, display a QR code icon that can be tapped
// to start the QR code scanner.
if (!_showScanner) if (!_showScanner)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
@ -138,6 +219,7 @@ class ReceiveScreenState extends State<ReceiveScreen> {
), ),
), ),
), ),
// If _showScanner is true, display the QR code scanner.
if (_showScanner) if (_showScanner)
Container( Container(
width: MediaQuery.of(context).size.width * 0.8, width: MediaQuery.of(context).size.width * 0.8,
@ -148,7 +230,9 @@ class ReceiveScreenState extends State<ReceiveScreen> {
), ),
child: _buildQRScanner(), child: _buildQRScanner(),
), ),
// Add some spacing between the scanner and the text field.
const SizedBox(height: 32), const SizedBox(height: 32),
// Display a text field for entering the transfer name.
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox( child: SizedBox(
@ -179,7 +263,9 @@ class ReceiveScreenState extends State<ReceiveScreen> {
), ),
), ),
), ),
// Add some spacing between the text field and the receive button.
const SizedBox(height: 16), const SizedBox(height: 16),
// Display an elevated button for initiating the receive process.
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Constants.textColor, backgroundColor: Constants.textColor,

View file

@ -9,20 +9,45 @@ import 'package:flutter_test_gui/src/rust/api/simple.dart';
// import 'package:flutter_test_gui/src/rust/frb_generated.dart'; // import 'package:flutter_test_gui/src/rust/frb_generated.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
/// Represents the screen for sending files.
///
/// This is a [StatefulWidget] that displays a screen for sending files.
/// It allows the user to select files to send and provides a name for the transfer.
/// The selected files are stored in a list and can be accessed by the [SendScreenState].
///
/// See also:
/// - [SendScreenState]
class SendScreen extends StatefulWidget { class SendScreen extends StatefulWidget {
/// Creates a [SendScreen].
///
/// The [key] parameter is used to identify the [SendScreen] widget.
const SendScreen({super.key}); const SendScreen({super.key});
/// Creates a [SendScreenState] to control the [SendScreen].
///
/// This method is called when a [SendScreen] widget is created.
/// It returns a new instance of [SendScreenState].
@override @override
SendScreenState createState() => SendScreenState(); SendScreenState createState() => SendScreenState();
} }
class SendScreenState extends State<SendScreen> { class SendScreenState extends State<SendScreen> {
/// List of selected files to send.
final List<XFile> _list = []; final List<XFile> _list = [];
/// Name of the transfer.
String transferName = ''; String transferName = '';
/// Indicates whether the user is currently dragging files.
bool _dragging = false; bool _dragging = false;
/// Opens the file picker and adds the selected files to [_list].
///
/// See also:
/// - [FilePicker.platform.pickFiles]
Future<void> openFilePicker() async { Future<void> openFilePicker() async {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true, // Erlaube die Auswahl mehrerer Dateien allowMultiple: true, // Allow selecting multiple files
); );
if (result != null) { if (result != null) {
@ -30,9 +55,15 @@ class SendScreenState extends State<SendScreen> {
} }
} }
/// Generates a random name for the transfer and navigates to the waiting screen.
///
/// See also:
/// - [generateRandomName]
/// - [WaitingScreen]
Future<void> _startTransfer() async { Future<void> _startTransfer() async {
final randomName = generateRandomName(); // Rust-Funktion aufrufen final randomName =
print('Zusammengefügter Text: $randomName'); generateRandomName(); // Call Rust function to generate random name
print('Generated transfer name: $randomName');
setState(() { setState(() {
transferName = randomName; transferName = randomName;
}); });
@ -43,42 +74,61 @@ class SendScreenState extends State<SendScreen> {
WaitingScreen(transferName: transferName, files: _list))); WaitingScreen(transferName: transferName, files: _list)));
} }
/// Builds the UI for the send screen.
///
/// Returns a [Scaffold] widget that contains a [Column] with two children:
/// - A [Center] widget that contains a [Stack] with a [GestureDetector] that
/// handles file picking and dragging.
/// - An [ElevatedButton] that triggers the transfer when pressed.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// Set the background color of the scaffold.
backgroundColor: Constants.backColor, backgroundColor: Constants.backColor,
// Build the body of the scaffold.
body: Column( body: Column(
// Align the children vertically to the center.
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Build the file picking and dragging UI.
Center( Center(
child: Stack( child: Stack(
children: [ children: [
// Build the gesture detector.
GestureDetector( GestureDetector(
// Handle file picking when the user taps.
onTap: openFilePicker, onTap: openFilePicker,
// Handle file dragging.
child: DropTarget( child: DropTarget(
// Add the selected files to the list when the user drops files.
onDragDone: (detail) { onDragDone: (detail) {
setState(() { setState(() {
_list.addAll(detail.files); _list.addAll(detail.files);
}); });
}, },
// Show the add icon when the user drags files over the drop area.
onDragEntered: (detail) { onDragEntered: (detail) {
setState(() { setState(() {
_dragging = true; _dragging = true;
}); });
}, },
// Hide the add icon when the user stops dragging files.
onDragExited: (detail) { onDragExited: (detail) {
setState(() { setState(() {
_dragging = false; _dragging = false;
}); });
}, },
// Build the drop area UI.
child: Column( child: Column(
children: [ children: [
// Build the circular container for the drop area.
Container( Container(
height: 200, height: 200,
width: 200, width: 200,
decoration: const BoxDecoration( decoration: const BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Constants.textColor), color: Constants.textColor),
// Show the add icon when the user is dragging files.
child: _dragging child: _dragging
? const Center( ? const Center(
child: Icon( child: Icon(
@ -87,6 +137,7 @@ class SendScreenState extends State<SendScreen> {
size: 200, size: 200,
), ),
) )
// Show the upload icon when the user is not dragging files.
: const Center( : const Center(
child: Icon( child: Icon(
Icons.upload_rounded, Icons.upload_rounded,
@ -95,6 +146,7 @@ class SendScreenState extends State<SendScreen> {
), ),
), ),
), ),
// Add some spacing between the drop area and the send button.
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
), ),
@ -103,17 +155,23 @@ class SendScreenState extends State<SendScreen> {
], ],
), ),
), ),
// Build the send button.
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
// Set the background color of the button.
backgroundColor: Constants.textColor, backgroundColor: Constants.textColor,
// Set the text color of the button.
foregroundColor: Constants.backColor, foregroundColor: Constants.backColor,
// Set the shape of the button.
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
), ),
// Trigger the transfer when the user presses the button.
onPressed: () { onPressed: () {
_startTransfer(); _startTransfer();
}, },
// Set the text of the button.
child: const Text("Send"), child: const Text("Send"),
), ),
], ],

View file

@ -2,12 +2,26 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
/// Screen for displaying and editing the app's settings.
///
/// This screen allows the user to view and edit the app's settings.
/// The settings include the app environment, host, port, origin, and relay.
/// The settings are stored in SharedPreferences.
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
/// Constructs a [SettingsScreen].
const SettingsScreen({Key? key}) : super(key: key);
@override @override
_SettingsScreenState createState() => _SettingsScreenState(); _SettingsScreenState createState() => _SettingsScreenState();
} }
/// State for the [SettingsScreen].
///
/// This state manages the text editing controllers for the app's settings.
/// It also loads the settings from SharedPreferences when the widget is
/// first created.
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
// Controllers for the text fields.
final TextEditingController _appEnvironmentController = final TextEditingController _appEnvironmentController =
TextEditingController(); TextEditingController();
final TextEditingController _appHostController = TextEditingController(); final TextEditingController _appHostController = TextEditingController();
@ -15,21 +29,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _appOriginController = TextEditingController(); final TextEditingController _appOriginController = TextEditingController();
final TextEditingController _appRelayController = TextEditingController(); final TextEditingController _appRelayController = TextEditingController();
/// Loads the app settings from SharedPreferences when the widget is created.
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadSettings(); loadSettings();
} }
/// Loads the app settings from SharedPreferences.
///
/// This function retrieves the app settings from SharedPreferences and
/// sets the text of the corresponding text editing controllers.
/// If any of the settings are not found in SharedPreferences, an empty
/// string is used instead.
Future<void> loadSettings() async { Future<void> loadSettings() async {
// Retrieve the SharedPreferences instance
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
// Retrieve the app settings from SharedPreferences
String appEnvironment = prefs.getString('app_environment') ?? ''; String appEnvironment = prefs.getString('app_environment') ?? '';
String appHost = prefs.getString('app_host') ?? ''; String appHost = prefs.getString('app_host') ?? '';
String appPort = prefs.getString('app_port') ?? ''; String appPort = prefs.getString('app_port') ?? '';
String appOrigin = prefs.getString('app_origin') ?? ''; String appOrigin = prefs.getString('app_origin') ?? '';
String appRelay = prefs.getString('app_relay') ?? ''; String appRelay = prefs.getString('app_relay') ?? '';
// Setzen Sie die Controller-Texte nach dem Laden der Einstellungen // Set the text of the corresponding text editing controllers
setState(() { setState(() {
_appEnvironmentController.text = appEnvironment; _appEnvironmentController.text = appEnvironment;
_appHostController.text = appHost; _appHostController.text = appHost;
@ -39,28 +63,49 @@ class _SettingsScreenState extends State<SettingsScreen> {
}); });
} }
/// Saves the app settings to SharedPreferences.
///
/// This function retrieves the text from the corresponding text editing controllers
/// and saves them to SharedPreferences.
/// If any of the settings are empty, it saves an empty string.
Future<void> saveSettings() async { Future<void> saveSettings() async {
// Retrieve the SharedPreferences instance
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('app_environment', _appEnvironmentController.text);
await prefs.setString('app_host', _appHostController.text); // Retrieve the text from the corresponding text editing controllers
await prefs.setString('app_port', _appPortController.text); String appEnvironment = _appEnvironmentController.text;
await prefs.setString('app_origin', _appOriginController.text); String appHost = _appHostController.text;
await prefs.setString('app_relay', _appRelayController.text); String appPort = _appPortController.text;
String appOrigin = _appOriginController.text;
String appRelay = _appRelayController.text;
// Save the app settings to SharedPreferences
await prefs.setString('app_environment', appEnvironment);
await prefs.setString('app_host', appHost);
await prefs.setString('app_port', appPort);
await prefs.setString('app_origin', appOrigin);
await prefs.setString('app_relay', appRelay);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// Set the background color of the Scaffold
backgroundColor: Constants.backColor, backgroundColor: Constants.backColor,
appBar: AppBar( appBar: AppBar(
// Set the title of the AppBar
title: const Text('Settings'), title: const Text('Settings'),
// Set the background color of the AppBar
backgroundColor: const Color(0xFF292c3c), //0xFF282C34), backgroundColor: const Color(0xFF292c3c), //0xFF282C34),
// Set the foreground color of the AppBar
foregroundColor: Constants.textColor, foregroundColor: Constants.textColor,
), ),
body: Padding( body: Padding(
// Set the padding around the body of the Scaffold
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
// Create a TextField for the 'App Environment' setting
TextField( TextField(
controller: _appEnvironmentController, controller: _appEnvironmentController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -68,6 +113,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
labelStyle: TextStyle(color: Constants.highlightColor)), labelStyle: TextStyle(color: Constants.highlightColor)),
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
// Create a TextField for the 'App Host' setting
TextField( TextField(
controller: _appHostController, controller: _appHostController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -75,6 +121,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
labelStyle: TextStyle(color: Constants.highlightColor)), labelStyle: TextStyle(color: Constants.highlightColor)),
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
// Create a TextField for the 'App Port' setting
TextField( TextField(
controller: _appPortController, controller: _appPortController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -82,6 +129,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
labelStyle: TextStyle(color: Constants.highlightColor)), labelStyle: TextStyle(color: Constants.highlightColor)),
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
// Create a TextField for the 'App Origin' setting
TextField( TextField(
controller: _appOriginController, controller: _appOriginController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -89,6 +137,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
labelStyle: TextStyle(color: Constants.highlightColor)), labelStyle: TextStyle(color: Constants.highlightColor)),
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
// Create a TextField for the 'App Relay' setting
TextField( TextField(
controller: _appRelayController, controller: _appRelayController,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -97,6 +146,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
style: const TextStyle(color: Constants.textColor), style: const TextStyle(color: Constants.textColor),
), ),
Spacer(), Spacer(),
// Create an ElevatedButton to save the settings and return to the previous screen
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Constants.textColor, backgroundColor: Constants.textColor,

View file

@ -2,18 +2,30 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test_gui/main.dart'; import 'package:flutter_test_gui/main.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:cross_file/cross_file.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_test_gui/src/rust/api/simple.dart'; import 'package:flutter_test_gui/src/rust/api/simple.dart';
// import 'package:flutter_test_gui/src/rust/frb_generated.dart'; // import 'package:flutter_test_gui/src/rust/frb_generated.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
/// Represents the screen for transferring files.
///
/// This is a [StatefulWidget] that displays a screen for transferring files.
/// It takes in two parameters:
/// - [transferName]: The name of the transfer.
/// - [directory]: The directory containing the files to be transferred.
class TransferScreen extends StatefulWidget { class TransferScreen extends StatefulWidget {
// The name of the transfer.
final String transferName; final String transferName;
// The directory containing the files to be transferred.
final String directory; final String directory;
/// Creates a [TransferScreen] widget.
///
/// The [transferName] and [directory] parameters are required.
///
/// The [key] parameter is optional.
const TransferScreen( const TransferScreen(
{Key? key, required this.transferName, required this.directory}) {Key? key, required this.transferName, required this.directory})
: super(key: key); : super(key: key);
@ -23,57 +35,92 @@ class TransferScreen extends StatefulWidget {
} }
class TransferScreenState extends State<TransferScreen> { class TransferScreenState extends State<TransferScreen> {
// The origin of the application.
String appOrigin = ''; String appOrigin = '';
// The input value of the transfer name.
String inputValue = ''; String inputValue = '';
@override @override
void initState() { void initState() {
// Call the loadSettings function to load the settings.
super.initState(); super.initState();
loadSettings().then((_) => callStartReceiver(appOrigin)); loadSettings().then((_) => callStartReceiver(appOrigin));
} }
/// Loads the settings from the SharedPreferences.
///
/// It retrieves the value of 'app_origin' from the SharedPreferences and
/// assigns it to the [appOrigin] variable. If the value is not present, it
/// assigns a default value of 'wss://caesar-transfer-iu.shuttleapp.rs'.
Future<void> loadSettings() async { Future<void> loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
appOrigin = prefs.getString('app_origin') ?? appOrigin = prefs.getString('app_origin') ??
'wss://caesar-transfer-iu.shuttleapp.rs'; 'wss://caesar-transfer-iu.shuttleapp.rs';
} }
/// Calls the start transfer function with the given [appOrigin].
///
/// It calls the _startTransfer function with the [appOrigin] parameter.
Future<void> callStartReceiver(String appOrigin) async { Future<void> callStartReceiver(String appOrigin) async {
_startTransfer(appOrigin); _startTransfer(appOrigin);
} }
/// Starts the transfer with the given [appOrigin].
///
/// If the transfer name is not empty, it checks if the platform is Android.
/// If it is, it requests the ManageExternalStorage permission. If the
/// permission is granted, it starts the receiver using the startRustReceiver
/// function. If the permission is not granted, it navigates to the
/// MyHomePage. If the platform is not Android, it starts the receiver
/// directly. If the transfer name is empty, it does not start the receiver.
///
/// Parameters:
/// - appOrigin: The origin of the application.
Future<void> _startTransfer(String appOrigin) async { Future<void> _startTransfer(String appOrigin) async {
// Get the input value from the widget.
final input = widget.transferName; final input = widget.transferName;
String filePath = widget.directory; String filePath = widget.directory;
// If the input value is not empty, start the transfer.
if (input.isNotEmpty) { if (input.isNotEmpty) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (await _requestPermission(Permission.manageExternalStorage)) { // Check if the ManageExternalStorage permission is granted.
try { //if (await _requestPermission(Permission.manageExternalStorage)) {
final outcome = await startRustReceiver( try {
filepath: filePath, transfername: input, relay: appOrigin); // Start the receiver with the given parameters.
print('Ergebnis von Rust: $outcome'); final outcome = await startRustReceiver(
} catch (e) { filepath: filePath, transfername: input, relay: appOrigin);
print('Fehler beim Starten des Receivers: $e'); print('Ergebnis von Rust: $outcome');
} } catch (e) {
Navigator.push( // If an error occurs, print the error message.
context, print('Fehler beim Starten des Receivers: $e');
MaterialPageRoute( }
builder: (context) => // Navigate to the MyHomePage.
const MyHomePage(title: 'Caesar Transfer'))); Navigator.push(
} else { context,
Navigator.push( MaterialPageRoute(
context, builder: (context) =>
MaterialPageRoute( const MyHomePage(title: 'Caesar Transfer')));
builder: (context) => //} else {
const MyHomePage(title: 'Caesar Transfer'))); // // If the permission is not granted, navigate to the MyHomePage.
} // Navigator.push(
} else { // context,
// MaterialPageRoute(
// builder: (context) =>
// const MyHomePage(title: 'Caesar Transfer')));
//}
} else {
// If the platform is not Android, start the receiver directly.
try { try {
final outcome = await startRustReceiver( final outcome = await startRustReceiver(
filepath: filePath, transfername: input, relay: appOrigin); filepath: filePath, transfername: input, relay: appOrigin);
print('Ergebnis von Rust: $outcome'); print('Ergebnis von Rust: $outcome');
} catch (e) { } catch (e) {
// If an error occurs, print the error message.
print('Fehler beim Starten des Receivers: $e'); print('Fehler beim Starten des Receivers: $e');
} }
// Navigate to the MyHomePage.
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -81,36 +128,74 @@ class TransferScreenState extends State<TransferScreen> {
const MyHomePage(title: 'Caesar Transfer'))); const MyHomePage(title: 'Caesar Transfer')));
} }
} }
// Print the app origin.
print("Transfer startet with app_origin: $appOrigin"); print("Transfer startet with app_origin: $appOrigin");
} }
/// Requests the given [permission] and returns a `Future` of `bool` indicating
/// whether the permission is granted.
///
/// If the permission is already granted, it returns `true`. If the permission
/// is not granted, it requests the permission and returns `true` if the
/// permission is granted successfully, otherwise it returns `false`.
///
/// Parameters:
/// - permission: The permission to be requested.
///
/// Returns:
/// A `Future` of `bool` indicating whether the permission is granted.
Future<bool> _requestPermission(Permission permission) async { Future<bool> _requestPermission(Permission permission) async {
// If the permission is already granted, return true.
if (await permission.isGranted) { if (await permission.isGranted) {
return true; return true;
} else { } else {
// Request the permission and get the result.
var result = await permission.request(); var result = await permission.request();
if (result == PermissionStatus.granted) { // If the permission is granted, return true. Otherwise, return false.
return true; return result == PermissionStatus.granted;
} else {
return false;
}
} }
} }
@override @override
/// Builds the widget tree for the TransferScreen.
///
/// This method builds a widget tree for the TransferScreen. It returns a
/// Scaffold widget with a background color set to Constants.backColor. The
/// body of the scaffold is a Center widget that contains a Column widget.
/// The Column widget has its mainAxisAlignment set to MainAxisAlignment.center.
/// It contains three children: a Text widget displaying the transferName, a
/// Text widget with the text "Transfer in Progress", and a SizedBox widget
/// with a height of 32. The SizedBox widget is followed by a Center widget
/// that contains an Icon widget with the icon Icons.cloud_download_rounded,
/// its color set to Constants.highlightColor, and its size set to 200.
///
/// Returns:
/// The widget tree for the TransferScreen.
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// Set the background color of the Scaffold widget.
backgroundColor: Constants.backColor, backgroundColor: Constants.backColor,
body: Center( body: Center(
// The body of the Scaffold widget.
child: Column( child: Column(
// The Column widget has its mainAxisAlignment set to
// MainAxisAlignment.center.
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// A Text widget displaying the transferName.
Text( Text(
widget.transferName, widget.transferName,
style: const TextStyle(color: Colors.white, fontSize: 24), style: const TextStyle(
color: Colors.white,
fontSize: 24,
), ),
),
// A Text widget with the text "Transfer in Progress".
Text("Transfer in Progress"), Text("Transfer in Progress"),
// A SizedBox widget with a height of 32.
const SizedBox(height: 32), const SizedBox(height: 32),
// A Center widget containing an Icon widget.
const Center( const Center(
child: Icon( child: Icon(
Icons.cloud_download_rounded, Icons.cloud_download_rounded,

View file

@ -8,63 +8,135 @@ import 'package:flutter_test_gui/src/rust/api/simple.dart';
// import 'package:flutter_test_gui/src/rust/frb_generated.dart'; // import 'package:flutter_test_gui/src/rust/frb_generated.dart';
import 'package:flutter_test_gui/consts/consts.dart'; import 'package:flutter_test_gui/consts/consts.dart';
/// Represents the screen for displaying the waiting state.
///
/// This is a [StatefulWidget] that displays a screen for the waiting state.
/// It takes in two parameters:
/// - [transferName]: The name of the transfer.
/// - [files]: The list of files being transferred.
class WaitingScreen extends StatefulWidget { class WaitingScreen extends StatefulWidget {
// The list of files being transferred.
final List<XFile> files; final List<XFile> files;
// The name of the transfer.
final String transferName; final String transferName;
/// Creates a new instance of the [WaitingScreen] widget.
///
/// The [transferName] parameter is the name of the transfer.
/// The [files] parameter is the list of files being transferred.
const WaitingScreen( const WaitingScreen(
{Key? key, required this.transferName, required this.files}) {Key? key, required this.transferName, required this.files})
: super(key: key); : super(key: key);
/// Creates the mutable state for this widget at a given location in the tree.
///
/// See also:
/// - [StatefulWidget.createState]
@override @override
WaitingScreenState createState() => WaitingScreenState(); WaitingScreenState createState() => WaitingScreenState();
} }
class WaitingScreenState extends State<WaitingScreen> { class WaitingScreenState extends State<WaitingScreen> {
// The origin of the app.
String appOrigin = ''; String appOrigin = '';
/// Initializes the state of the widget.
///
/// This function is called when the widget is first created.
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Load the settings and then start the transfer.
loadSettings().then((_) => callStartSender(appOrigin)); loadSettings().then((_) => callStartSender(appOrigin));
} }
/// Loads the settings.
///
/// This function loads the settings from the shared preferences.
/// It retrieves the app origin from the shared preferences and assigns it to
/// the [appOrigin] variable.
///
/// Returns a [Future] that completes when the settings are loaded.
Future<void> loadSettings() async { Future<void> loadSettings() async {
// Get the shared preferences instance.
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
// Get the app origin from the shared preferences.
// If the app origin is not found, use the default value.
appOrigin = prefs.getString('app_origin') ?? appOrigin = prefs.getString('app_origin') ??
'wss://caesar-transfer-iu.shuttleapp.rs'; 'wss://caesar-transfer-iu.shuttleapp.rs';
} }
/// Calls the start sender function.
///
/// This function calls the [_startTransfer] function with the provided
/// [appOrigin].
///
/// Parameters:
/// - appOrigin: The origin of the app.
Future<void> callStartSender(String appOrigin) async { Future<void> callStartSender(String appOrigin) async {
_startTransfer(appOrigin); _startTransfer(appOrigin);
} }
/// Starts the transfer.
///
/// This function converts the list of files to a list of file names and then
/// calls the [startRustSender] function with the provided parameters.
///
/// Parameters:
/// - appOrigin: The origin of the app.
Future<void> _startTransfer(String appOrigin) async { Future<void> _startTransfer(String appOrigin) async {
// Convert the list of files to a list of file names.
List<String> fileNames = widget.files.map((file) => file.path).toList(); List<String> fileNames = widget.files.map((file) => file.path).toList();
// Start the transfer.
final outcome = await startRustSender( final outcome = await startRustSender(
name: widget.transferName, relay: appOrigin, files: fileNames); name: widget.transferName, relay: appOrigin, files: fileNames);
// Navigate to the home page.
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => MyHomePage(title: 'Caesar Transfer'))); builder: (context) => MyHomePage(title: 'Caesar Transfer')));
} }
/// Builds the waiting screen widget.
///
/// This widget displays the transfer name and a QR code representing the
/// transfer name.
///
/// Returns:
/// A [Scaffold] widget containing the waiting screen UI.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// Set the background color of the scaffold.
backgroundColor: Constants.backColor, backgroundColor: Constants.backColor,
// Center the content of the scaffold.
body: Center( body: Center(
child: Column( child: Column(
// Align the children of the column in the center.
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Display the transfer name.
Text( Text(
widget.transferName, widget.transferName,
// Set the text style for the transfer name.
style: const TextStyle(color: Colors.white, fontSize: 24), style: const TextStyle(color: Colors.white, fontSize: 24),
), ),
// Add spacing between the transfer name and the QR code.
const SizedBox(height: 32), const SizedBox(height: 32),
// Display a QR code representing the transfer name.
QrImageView( QrImageView(
// Set the data to be encoded in the QR code.
data: widget.transferName, data: widget.transferName,
// Set the version of the QR code.
version: QrVersions.auto, version: QrVersions.auto,
// Set the size of the QR code.
size: 200, size: 200,
// Set the foreground color of the QR code.
foregroundColor: Constants.highlightColor, foregroundColor: Constants.highlightColor,
), ),
], ],