Compare commits

..

27 commits

Author SHA1 Message Date
git
e3c6cbd231 Merge pull request 'chore: Configure Renovate' (#112) from renovate/configure into main
Some checks failed
Cargo Build & Test / Rust project - latest (push) Has been cancelled
Reviewed-on: #112
2025-09-07 12:39:23 +02:00
Renovate Bot
7829b76080 Add renovate.json
Some checks failed
Cargo Build & Test / Rust project - latest (push) Has been cancelled
Cargo Build & Test / Rust project - latest (pull_request) Has been cancelled
2025-08-18 20:26:36 +00:00
PatrykHegenberg
b6329eb698
Update README.md 2024-05-31 10:04:47 +02:00
PatrykHegenberg
62f84255e3
Update README.md 2024-05-31 07:23:53 +02:00
PatrykHegenberg
d5cf4d86f2
Create LICENSE 2024-05-31 07:11:06 +02:00
PatrykHegenberg
f19648a386
Update build_and_test_on_push.yml 2024-05-30 09:22:23 +02:00
3abff684ad test(caesar-core,sender): added missing test for replace_protocol 2024-05-30 09:17:50 +02:00
PatrykHegenberg
f76f89bf9f
Create build_and_test_on_push.yml 2024-05-30 09:16:25 +02:00
0465851ec5 docs: added project code documentation 2024-05-30 08:55:59 +02:00
07584a48a1 version: updated version and removed non existing packages 2024-05-30 00:26:16 +02:00
4a3b1a7996 refactor(project): deleted not needed folders 2024-05-30 00:11:47 +02:00
0d354efb8e docs(project): added docstrings to all code files 2024-05-29 23:55:47 +02:00
47acdec22a refactor(gui): removed old rinf based flutter gui 2024-05-29 21:48:37 +02:00
PatrykHegenberg
4697cf35db
Merge pull request #111 from PatrykHegenberg/91-implement-the-gui-for-native-desktop-apps-in-flutter
91 implement the gui for native desktop apps in flutter
2024-05-29 21:27:19 +02:00
PatrykHegenberg
fc5d8efed6
Merge branch 'main' into 91-implement-the-gui-for-native-desktop-apps-in-flutter 2024-05-29 21:26:34 +02:00
4af2255d2f refactor(project): cleaned up with clippy 2024-05-29 20:14:34 +02:00
cd43fd6041 feature(gui): adjusted color settings for buttons and settings screen 2024-05-27 22:25:13 +02:00
d9501024db refactor(gui): cleaned up not needed code 2024-05-27 21:55:35 +02:00
0416ab6dab refactor(core,gui): make all needed changes to send and receive files on linux and android 2024-05-27 21:09:24 +02:00
351b7b9323 refactor(flutter_gui); removed not needed parts 2024-05-25 21:04:16 +02:00
1586818bd6 refactor(ceasar-core): changed from using openssl to rustls 2024-05-24 16:47:44 +02:00
1b83c0b5ff refactor(flutter_gui): moved constants to seperate file 2024-05-17 13:50:53 +02:00
f0f67332a0 feature(flutter_gui): change label alignment on receive screen 2024-05-17 12:03:40 +02:00
fbb212f383 feature(flutter_gui): add file storage location for android and change screen change after transfer 2024-05-17 10:32:11 +02:00
fbb181d95b refactir(gui): added flutter version with flutter_rust_bridge 2024-05-16 20:25:21 +02:00
f1e48f6d12 feature(gui): add needed function for communication between gui and core 2024-05-16 07:43:11 +02:00
113e1cfe53 feature(gui): first basic attempt for a gui frontend in flutter 2024-05-15 19:46:48 +02:00
3197 changed files with 77262 additions and 18730 deletions

View file

@ -0,0 +1,26 @@
name: Cargo Build & Test
on:
push:
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Rust project - latest
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v3
- run: sudo apt-get -y install protobuf-compiler
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose

3486
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,3 @@
[workspace]
resolver = "2"
members = [
"caesar-cli",
"caesar-core",
"caesar-desktop",
"caesar-mobile",
"caesar-shuttle",
"caesar-tui",
"caesar-tauri-angular/src-tauri"
]
members = ["caesar-cli", "caesar-core", "caesar-shuttle", "flutter_test_lib"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,22 +1,28 @@
# Caesar
This program provides a simple relay server that can be controlled via the command line.
# Caesar-Transfer
![caesar-gui-send-linux](https://github.com/PatrykHegenberg/caesar-transfer/assets/112555272/8e8bc3a9-cf2d-4a46-8280-fe88304e0a84)
This program provides a simple end-to_end encrypted filesharing system.
Either the cli version or the gui version can be used for this.
## Prerequisites
Rust toolchain installed
## Installation
1. Clone the repository:
```bash
git clone https://github.com/your-username/caesar.git
git clone https://github.com/PatrykHegenberg/caesar-transfer.git
```
2. Change to the project directory:
```bash
cd caesar
cd caesar-transfer
```
3. Build the program:
```bash
cargo build --release
cargo build --bin caesar --release
```
## Usage
### cli
The program offers the following commands:
`serve`
Starts the relay server.
@ -28,7 +34,7 @@ You can optionally specify the listening address and port using flags:
```bash
./target/release/caesar serve -p 8080 -l 192.168.1.100
```
By default, the server listens on 0.0.0.0:1323.
By default, the server listens on 0.0.0.0:8000.
`send`
Sends data through the relay server.
@ -41,7 +47,18 @@ Receives data through the relay server.
```bash
./target/release/caesar receive
```
### GUI
To use the Gui version of Ceasar-Transfer, you can download the version that suits you under Releases. Currently supported operating systems are Windows, Linux and Android.
#### Desktop
Copy the folder contained in the zip/tar file to a folder of your choice and add the path to it to your PATH variable.
Start the application and configure your relay server in the settings.
#### Android
As the Android version is currently in beta status, the APK must also be downloaded from the release page.
Open it with your smartphone's file manager and install it.
Start the application and configure your relay server in the settings.
## Help
For more information about the commands and arguments, use:
```bash
@ -57,12 +74,12 @@ Start the relay server:
Open a send window in another terminal:
```bash
./target/release/caesar send
./target/release/caesar -r ws://0.0.0.0:8000 send
```
Open a receive window in another terminal:
```bash
./target/release/caesar receive
./target/release/caesar -r ws://0.0.0.0:8000 receive
```
Now you can test the functionality of the relay server.
Now you can test the functionality of the relay server using the cli version.

View file

@ -1,6 +1,6 @@
[package]
name = "caesar-cli"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz"]
@ -8,8 +8,6 @@ authors = ["Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz"]
name = "caesar"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
caesar-core = { path = "../caesar-core" }
tokio = { version = "1.28.1", features = ["full"] }

View file

@ -1,17 +1,28 @@
use caesar_core::receiver;
use caesar_core::relay;
use caesar_core::sender;
use caesar_core::sender::util::generate_random_name;
use caesar_core::{receiver, sender::util::generate_random_name};
use clap::{Parser, Subcommand};
use std::{env, sync::Arc};
use tracing::debug;
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)]
#[command(version = env!("CARGO_PKG_VERSION"), about = "Send and receive files securely")]
#[command(long_about = None)]
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)]
pub command: Option<Commands>,
}
@ -33,10 +44,6 @@ pub enum Commands {
#[arg(short, long)]
relay: Option<String>,
/// Overwrite existing Files
#[arg(short, long)]
overwrite: bool,
/// Name of Transfer to download files
#[arg(value_name = "Transfer_Name")]
name: String,
@ -52,49 +59,93 @@ 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 {
/// Creates a new instance of `Args` by calling the `new` method.
///
/// # Returns
///
/// A new instance of `Args`.
fn default() -> Self {
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 {
/// Creates a new instance of `Args` by calling the `parse` method.
pub fn new() -> Self {
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>> {
// Retrieve the global configuration
let cfg = &GLOBAL_CONFIG;
debug!("args: {:#?}", self);
// Match on the `command` field of `Args` to execute the corresponding command
match &self.command {
// Command to send files to the receiver or relay server
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();
// Create Arc wrappers for the relay address and file paths
let relay_arc = Arc::new(relay_string);
let files_arc = Arc::new(files.to_vec());
let transfer_name = generate_random_name();
sender::start_sender(relay_arc, files_arc, transfer_name).await;
// Generate a 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;
}
// Command to receive files from the sender with the matching password
Some(Commands::Receive {
relay,
overwrite: _,
name,
}) => {
// Print the received transfer name
println!("Receive for {name:?}");
receiver::start_receiver(relay.as_deref().unwrap_or(&cfg.app_origin), name).await;
// Start the receiver with the current directory, relay address, and transfer name
let _ = receiver::start_receiver(
".".to_string(),
relay.as_deref().unwrap_or(&cfg.app_origin),
name,
)
.await;
}
// Command to start a relay server
Some(Commands::Serve {
port,
listen_address,
}) => {
println!("Serve with address '{listen_address:?}' and '{port:?}'");
// Create a string representation of the listen address
let address: String = listen_address
.as_deref()
.unwrap_or(&cfg.app_host)
.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: i32 = port_value;
// Start the relay server with the port and listen address
relay::server::start_ws(&port, &address).await;
}
// No command provided
None => {}
}
Ok(())

View file

@ -1,25 +1,70 @@
use lazy_static::lazy_static;
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)]
pub struct CaesarConfig {
/// The environment in which the application is running.
///
/// Possible values are "production", "staging", or "development".
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,
/// The port on which the application is listening.
///
/// This is typically a string representation of a port number.
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,
/// The relay endpoint of the application.
///
/// This is typically a combination of a hostname and port.
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,
}
/// 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 {
/// 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 {
CaesarConfig {
app_environment: "production".to_string(),
app_host: "localhost".to_string(),
app_port: "8000".to_string(),
app_origin: "wss://caesar-transfer-iu.shuttleapp.rs".to_string(),
app_relay: "localhost:8000".to_string(),
rust_log: "info".to_string(),
app_environment: "production".to_string(), // The environment in which the application is running.
app_host: "0.0.0.0".to_string(), // The host on which the application is running.
app_port: "8000".to_string(), // The port on which the application is listening.
app_origin: "wss://caesar-transfer-iu.shuttleapp.rs".to_string(), // The origin of the application.
app_relay: "0.0.0.0:8000".to_string(), // The relay endpoint of the application.
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 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]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load environment variables from the `.env` file.
dotenv().ok();
// Initialize the logging subscriber.
// It configures the logging level based on the `RUST_LOG` environment variable.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
// Parse the command line arguments.
let args = Args::new();
// Run the application.
// If an error occurs, log the error message.
if let Err(e) = args.run().await {
error!("{e}");
}
Ok(())
}

View file

@ -1,10 +1,13 @@
[package]
name = "caesar-core"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
build = "src/build.rs"
authors = ["Manuel Keidel", "Patryk Hegenberg", "Krzysztof Stankiewicz"]
[lib]
crate-type = ["lib", "cdylib", "staticlib"]
[dependencies]
futures-util = "0.3"
tungstenite = "0.21.0"

View file

@ -23,74 +23,187 @@ const DESTINATION: u8 = 0;
const NONCE_SIZE: usize = 12;
#[cfg(target_os = "android")]
const FILE_PATH_PREFIX: &str = "/storage/emulated/0/Documents";
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 {
/// The name of the file.
name: String,
/// The total size of the file in bytes.
size: u64,
/// The number of bytes that have been transferred so far.
progress: u64,
/// The file handle for reading and writing the 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 {
/// The HMAC key used for authentication.
hmac: Vec<u8>,
/// The sender used for sending packets.
sender: Sender,
/// The ephemeral secret key used for key agreement.
key: EphemeralSecret,
/// The shared key used for encryption.
shared_key: Option<Aes128Gcm>,
/// The list of files being transferred.
files: Vec<File>,
/// The sequence number of the last received packet.
sequence: u32,
/// The index of the current file being transferred.
index: usize,
/// The number of bytes transferred so far.
progress: u64,
/// The total length of the file being transferred.
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 {
// Check if the size of the room is provided
if size.is_none() {
// Return an error if the join room packet is invalid
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.");
// Return a continue status to indicate that the operation was successful
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 {
// Return an error with the provided error 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 {
// Check if there are any files being transferred with less than 100% progress
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!("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())
} else {
// Return an exit status to indicate that the operation was successful
Status::Exit()
}
}
fn on_list(context: &mut Context, list: ListPacket) -> Status {
/// 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 {
// Check if the shared key is established
if context.shared_key.is_none() {
return Status::Err("Invalid list packet: no shared key established".into());
}
#[cfg(target_os = "android")]
let mut prefix = std::path::PathBuf::from(std::env::current_exe().unwrap());
// #[cfg(target_os = "android")]
// prefix.pop();
// #[cfg(target_os = "android")]
// println!("prefix: {:?}", prefix);
// Iterate over the entries in the list packet
for entry in list.entries {
// Sanitize the filename to prevent directory traversal attacks
let path = sanitize_filename::sanitize(entry.name.clone());
#[cfg(target_os = "android")]
let file_path = format!("{}/{}", FILE_PATH_PREFIX, path);
// let file_path = format!("{}/{}", prefix.display(), path);
// Construct the file path
let file_path = format!("{}/{}", filepath, path);
#[cfg(target_os = "android")]
// Check if the file already exists
if Path::new(&file_path).exists() {
return Status::Err(format!("The file '{}' already exists.", path));
}
#[cfg(target_os = "android")]
// Create a new file
let handle = match fs::File::create(&file_path) {
Ok(handle) => handle,
Err(error) => {
@ -100,22 +213,8 @@ fn on_list(context: &mut Context, list: ListPacket) -> Status {
));
}
};
#[cfg(not(target_os = "android"))]
if Path::new(&path).exists() {
return Status::Err(format!("The file '{}' already exists.", path));
}
#[cfg(not(target_os = "android"))]
let handle = match fs::File::create(&path) {
Ok(handle) => handle,
Err(error) => {
return Status::Err(format!(
"Error: Failed to create file '{}': {}",
path, error
));
}
};
// Create a new file object and add it to the context
let file = File {
name: entry.name,
size: entry.size,
@ -126,6 +225,7 @@ fn on_list(context: &mut Context, list: ListPacket) -> Status {
context.files.push(file);
}
// Reset the context for the next file transfer
context.index = 0;
context.progress = 0;
context.sequence = 0;
@ -134,11 +234,28 @@ fn on_list(context: &mut Context, list: ListPacket) -> Status {
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 {
// Check if the shared key is established
if context.shared_key.is_none() {
return Status::Err("Invalid chunk packet: no shared key established".into());
}
// Verify the sequence number
if chunk.sequence != context.sequence {
return Status::Err(format!(
"Expected sequence {}, but got {}.",
@ -146,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 {
return Status::Err("Invalid file index.".into());
};
// Update the file's length
context.length += chunk.chunk.len() as u64;
// Increment the sequence number
context.sequence += 1;
// Write the chunk to the file
file.handle.write(&chunk.chunk).unwrap();
// Update the file's progress
file.progress = (context.length * 100) / file.size;
// Send progress updates if necessary
if file.progress == 100 || file.progress - context.progress >= 1 || chunk.sequence == 0 {
context.progress = file.progress;
@ -176,6 +299,7 @@ fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
std::io::Write::flush(&mut stdout()).unwrap();
}
// Handle the end of a file transfer
if file.size == context.length {
context.index += 1;
context.length = 0;
@ -188,59 +312,104 @@ fn on_chunk(context: &mut Context, chunk: ChunkPacket) -> Status {
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 {
// Check if the shared key is already established
if context.shared_key.is_some() {
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();
// Update the HMAC with the sender's public key
mac.update(&handshake.public_key);
// Verify the signature using the HMAC
let verification = mac.verify_slice(&handshake.signature);
if verification.is_err() {
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();
// Create a new HMAC instance using the shared key
let mut mac = Hmac::<Sha256>::new_from_slice(&context.hmac).unwrap();
// Update the HMAC with the receiver's public key
mac.update(&public_key);
// Generate the signature using the HMAC
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();
// Perform Diffie-Hellman key exchange
let shared_secret = context.key.diffie_hellman(&shared_public_key);
let shared_secret = shared_secret.raw_secret_bytes();
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 = <Aes128Gcm as aes_gcm::KeyInit>::new(shared_key);
// Create the handshake response packet
let handshake_response = HandshakeResponsePacket {
public_key,
signature,
};
// Send the handshake response packet to the sender
context
.sender
.send_packet(DESTINATION, Value::HandshakeResponse(handshake_response));
// Establish the shared key
context.shared_key = Some(shared_key);
Status::Continue()
}
fn on_message(context: &mut Context, message: WebSocketMessage) -> Status {
/// 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 {
// Handle text messages
match message.clone() {
WebSocketMessage::Text(text) => {
// Parse the JSON packet
let packet = match serde_json::from_str(&text) {
Ok(packet) => packet,
Err(_) => {
return Status::Continue();
}
};
// Handle different types of JSON packets
return match packet {
JsonPacketResponse::Join { size } => on_join_room(size),
JsonPacketResponse::Leave { index } => on_leave_room(context, index),
@ -248,7 +417,9 @@ fn on_message(context: &mut Context, message: WebSocketMessage) -> Status {
_ => Status::Err(format!("Unexpected json packet: {:?}", packet)),
};
}
// Handle binary messages
WebSocketMessage::Binary(data) => {
// Extract the data from the binary message
let data = &data[1..];
let data = if let Some(shared_key) = &context.shared_key {
@ -260,10 +431,12 @@ fn on_message(context: &mut Context, message: WebSocketMessage) -> Status {
data.to_vec()
};
// Decode the packet
let packet = Packet::decode(data.as_ref()).unwrap();
let value = packet.value.unwrap();
// Handle different types of packets
return match value {
Value::List(list) => on_list(context, list),
Value::List(list) => on_list(filepath, context, list),
Value::Chunk(chunk) => on_chunk(context, chunk),
Value::Handshake(handshake) => on_handshake(context, handshake),
_ => Status::Err(format!("Unexpected packet: {:?}", value)),
@ -272,10 +445,27 @@ fn on_message(context: &mut Context, message: WebSocketMessage) -> Status {
_ => (),
}
// Return an error status for invalid message types
Status::Err("Invalid message type".into())
}
pub async fn start(socket: Socket, fragment: &str) {
/// 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) {
let Some(index) = fragment.rfind('-') else {
println!("Error: The invite code '{}' is not valid.", fragment);
return;
@ -316,7 +506,7 @@ pub async fn start(socket: Socket, fragment: &str) {
let outgoing_handler = receiver.stream().map(Ok).forward(outgoing);
let incoming_handler = incoming.try_for_each(|message| {
match on_message(&mut context, message) {
match on_message(filepath.clone(), &mut context, message) {
Status::Exit() => {
context.sender.send_json_packet(JsonPacket::Leave);
println!("Transfer has completed.");
@ -338,6 +528,7 @@ pub async fn start(socket: Socket, fragment: &str) {
future::select(incoming_handler, outgoing_handler).await;
}
#[cfg(test)]
mod tests {
use super::*;
@ -412,58 +603,15 @@ mod tests {
};
let text_message = WebSocketMessage::Text(r#"{"type":"join","size":10}"#.to_string());
assert_eq!(on_message(&mut context, text_message), Status::Continue());
assert_eq!(
on_message("".to_string(), &mut context, text_message),
Status::Continue()
);
}
#[test]
fn test_on_chunk() {
let (sender, _) = flume::bounded(1000);
// let mut context = Context {
// hmac: vec![],
// sender: sender.clone(),
// key: EphemeralSecret::random(&mut OsRng),
// shared_key: Some(Aes128Gcm::new(Key::<Aes128Gcm>::from_slice(&[0u8; 16]))),
// files: vec![File {
// name: "file1.txt".to_string(),
// size: 100,
// progress: 0,
// handle: fs::File::create("file1.txt").unwrap(),
// }],
// sequence: 0,
// index: 0,
// progress: 0,
// length: 0,
// };
// let chunk_packet = ChunkPacket {
// sequence: 0,
// chunk: b"Hello, world!".to_vec(),
// };
// assert_eq!(on_chunk(&mut context, chunk_packet), Status::Continue());
// assert_eq!(context.sequence, 1);
// assert_eq!(context.length, 14);
// assert_eq!(context.progress, 14);
// let chunk_packet = ChunkPacket {
// sequence: 1,
// chunk: b"Hello, world!".to_vec(),
// };
// assert_eq!(
// on_chunk(&mut context, chunk_packet),
// Status::Err("Expected sequence 1, but got 1.".into())
// );
// context.files.clear();
// let chunk_packet = ChunkPacket {
// sequence: 0,
// chunk: b"Hello, world!".to_vec(),
// };
// assert_eq!(
// on_chunk(&mut context, chunk_packet),
// Status::Err("Invalid file index.".into())
// );
// Test a chunk packet with no shared key
let mut context = Context {
hmac: vec![],
sender,
@ -489,4 +637,4 @@ mod tests {
Status::Err("Invalid chunk packet: no shared key established".into())
);
}
}
}

View file

@ -5,31 +5,65 @@ use sha2::{Digest, Sha256};
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> {
// Convert the relay server URL and file name to strings
let url = String::from(relay);
let hashed_name = Sha256::digest(name.as_bytes());
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))
.await
// If the request fails, return an error with the reason
.map_err(|e| anyhow!("Failed to send GET request: {}", e))?;
// Parse the response body as JSON into a `TransferResponse` struct
resp.json::<TransferResponse>()
.await
// If the JSON parsing fails, return an error with the reason
.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<()> {
// Convert the relay server URL and file name to strings
let url = String::from(relay);
let hashed_name = Sha256::digest(name.as_bytes());
let hashed_string = hex::encode(hashed_name);
// Create a new HTTP client
let client = Client::new();
// Send a POST request to the relay server with the file name hash as a query parameter
let _ = client
.post(format!("{}/download_success/{}", url, hashed_string))
.send()
.await
// If the request fails, return an error with the reason
.map_err(|e| anyhow!("Failed to send POST request: {}", e))?;
// Return Ok(()) if the request was successful
Ok(())
}
}

View file

@ -1,8 +1,8 @@
pub mod client;
pub mod http_client;
use anyhow::{Result, anyhow};
use crate::{receiver::client as receiver, sender::util::replace_protocol};
use anyhow::{anyhow, Result};
use tokio_tungstenite::{
connect_async,
@ -10,7 +10,24 @@ use tokio_tungstenite::{
};
use tracing::{debug, error};
pub async fn start_receiver(relay: &str, name: &str) -> Result<()> {
/// 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<()> {
let http_url = replace_protocol(relay);
let res = http_client::download_info(http_url.as_str(), name)
.await
@ -18,13 +35,25 @@ pub async fn start_receiver(relay: &str, name: &str) -> Result<()> {
debug!("Got room_id from Server: {:?}", res);
let res_ip = String::from("ws://") + res.ip.as_str() + ":9000";
if let Err(local_err) = start_ws_com(res_ip.as_str(), res.local_room_id.as_str()).await {
#[cfg(not(target_os = "android"))]
if let Err(local_err) = start_ws_com(
filepath.clone(),
res_ip.as_str(),
res.local_room_id.as_str(),
)
.await
{
debug!("Failed to connect local: {local_err}");
if let Err(relay_err) = start_ws_com(relay, res.relay_room_id.as_str()).await {
if let Err(relay_err) = start_ws_com(filepath, relay, res.relay_room_id.as_str()).await {
debug!("Failed to connect remote: {relay_err}");
}
}
let _success = http_client::download_success(http_url.as_str(), name)
#[cfg(target_os = "android")]
if let Err(relay_err) = start_ws_com(filepath, relay, res.relay_room_id.as_str()).await {
debug!("Failed to connect remote: {relay_err}");
}
http_client::download_success(http_url.as_str(), name)
.await
.map_err(|e| anyhow!("Failed to download success: {}", e))?;
@ -32,33 +61,55 @@ pub async fn start_receiver(relay: &str, name: &str) -> Result<()> {
Ok(())
}
pub async fn start_ws_com(relay: &str, name: &str) -> Result<()> {
/// 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<()> {
// Construct the WebSocket URL by appending "/ws" to the relay URL.
let url = String::from(relay) + "/ws";
// Create a WebSocket request using the constructed URL.
let mut request = url
.into_client_request()
.map_err(|e| anyhow!("Failed to create request: {}", e))?;
// Set the "Origin" header of the request to the relay URL.
request
.headers_mut()
.insert("Origin", HeaderValue::from_str(relay).unwrap());
// Print a message indicating the attempt 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))
.await
{
Ok(Ok((socket, _))) => {
receiver::start(socket, name).await;
// Start the receiver process with the established WebSocket connection.
receiver::start(filepath, socket, name).await;
Ok(())
}
Ok(Err(e)) => {
// Log the failure to connect.
error!("Error: Failed to connect: {e:?}");
Err(Box::new(e))
}
Err(e) => {
// Log the timeout.
error!("Error: Timeout reached for local connection attempt");
Err(Box::new(e))
}?,
};
Ok(())
}
}

View file

@ -4,18 +4,38 @@ use tokio::sync::RwLock;
use crate::relay::room::Room;
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)]
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>,
/// Vector of transfers.
pub transfers: Vec<TransferResponse>,
}
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>> {
Arc::new(RwLock::new(AppState {
// Create a new instance of `AppState` with empty rooms and transfers.
let app_state = AppState {
rooms: HashMap::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 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>>>;
/// 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)]
pub struct Client {
/// The WebSocket sender for sending messages.
sender: Sender,
/// The optional room ID of the client.
///
/// This is used to identify the client's room.
room_id: Option<String>,
}
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 {
Client {
sender,
room_id: None,
sender, // The WebSocket sender for sending messages.
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) {
let mut sender = sender.lock().await;
if let Err(error) = sender.send(message).await {
error!("Failed to send message to the client: {}", error);
let mut sender = sender.lock().await; // Acquires a lock on the sender.
if let Err(error) = sender.send(message).await { // Sends the message.
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) {
// Serialize the packet to a string.
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;
}
/// 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) {
// Create an error packet with the given 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>) {
// Acquire a write lock on the server state.
let mut server = server.write().await;
// Check if the client is already in a room.
if server.rooms.iter().any(|(_, room)| {
room.senders
.iter()
@ -55,14 +123,16 @@ impl Client {
return;
}
// Set the room size and generate a room identifier if none is provided.
let size = Room::DEFAULT_ROOM_SIZE;
let room_id = match id {
Some(id) => id,
None => Uuid::new_v4().to_string(),
};
// Check if the room identifier already exists.
if server.rooms.contains_key(&room_id) {
drop(server);
drop(server); // Release the lock before returning.
return self
.send_error_packet(
@ -72,23 +142,37 @@ impl Client {
.await;
}
// Create a new room and add the client to it.
let mut room = Room::new(size);
room.senders.push(self.sender.clone());
// Insert the room into the server state.
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");
// Send the response packet to the client.
self.send_packet(self.sender.clone(), ResponsePacket::Create { id: room_id })
.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) {
let mut server = server.write().await;
// Check if the client is already in a room.
if server.rooms.iter().any(|(_, room)| {
room.senders
.iter()
@ -100,24 +184,29 @@ impl Client {
let Some(room) = server.rooms.get_mut(&room_id) else {
drop(server);
// Send an error packet to the client.
return self
.send_error_packet(self.sender.clone(), "The room does not exist.".to_string())
.await;
};
// Check if the room is full.
if room.senders.len() >= room.size {
drop(server);
// Send an error packet to the client.
return self
.send_error_packet(self.sender.clone(), "The room is full.".to_string())
.await;
}
// Add the client to the room.
room.senders.push(self.sender.clone());
self.room_id = Some(room_id);
let mut futures = vec![];
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) {
futures.push(self.send_packet(
sender.clone(),
@ -126,6 +215,7 @@ impl Client {
},
));
} 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 }));
}
}
@ -134,17 +224,34 @@ impl Client {
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>) {
// Acquire a write lock on the server state.
let mut server = server.write().await;
// Get the room ID of the current room.
let Some(room_id) = self.room_id.clone() else {
return;
};
// Get the mutable reference to the room.
let Some(room) = server.rooms.get_mut(&room_id) else {
return;
};
// Get the index of the client in the room.
let Some(index) = room
.senders
.iter()
@ -153,70 +260,94 @@ impl Client {
return;
};
// Remove the client from the room.
room.senders.remove(index);
self.room_id = None;
let mut futures = vec![];
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 }));
}
// If the room is empty, remove it from the server state.
if room.senders.is_empty() {
server.rooms.remove(&room_id);
}
drop(server);
// Wait for all the futures to complete.
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) {
// Match on the type of the message.
match message {
// If the message is text, parse it as a RequestPacket.
Message::Text(text) => {
let packet = match serde_json::from_str(&text) {
Ok(packet) => packet,
Err(_) => return,
Err(_) => return, // Return if the parsing fails.
};
// Match on the RequestPacket type and perform the corresponding action.
match packet {
RequestPacket::Create { id } => self.handle_create_room(server, id).await,
RequestPacket::Join { id } => self.handle_join_room(server, id).await,
RequestPacket::Leave => self.handle_leave_room(server).await,
}
}
// If the message is binary, handle it accordingly.
Message::Binary(_) => {
// Acquire a read lock on the server state.
let server = server.read().await;
// Get the room ID of the current room.
let Some(room_id) = &self.room_id else {
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 {
drop(server);
return;
return; // Return if the room does not exist.
};
// Get the index of the client in the room.
let Some(index) = room
.senders
.iter()
.position(|sender| Arc::ptr_eq(sender, &self.sender))
else {
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();
if data.is_empty() {
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();
// Get the destination from the first byte of the data.
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() {
let sender = room.senders[destination].clone();
@ -224,11 +355,12 @@ impl Client {
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) {
let mut futures = vec![];
for sender in &room.senders {
if Arc::ptr_eq(sender, &self.sender) {
continue;
continue; // Skip the current client.
}
futures.push(self.send(sender.clone(), Message::Binary(data.clone())));
@ -238,12 +370,15 @@ impl Client {
join_all(futures).await;
}
}
// If the message is Ping, print a message.
Message::Ping(_) => {
println!("Got Message Type Ping");
}
// If the message is Pong, print a message.
Message::Pong(_) => {
println!("Got Message Type Pong");
}
// If the message is Close, print a message and handle the close.
Message::Close(_) => {
println!("Got Message Type Close");
self.handle_close(server).await;

View file

@ -6,33 +6,75 @@ pub mod transfer;
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)]
#[serde(tag = "type", rename_all = "camelCase")]
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 {
// The ID of the room that the client wants to join.
/// The ID of the room that the client wants to join.
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 {
/// The ID of the new room. If it is `None`, a random ID will be generated.
id: Option<String>,
},
/// The client wants to leave the current room.
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)]
#[serde(tag = "type", rename_all = "camelCase")]
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 {
/// The size of the room. If it is `None`, the size is unknown.
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<usize>,
},
/// The client has created a new room.
///
/// The `id` field is the ID of the new room.
Create {
/// The ID of the new room.
id: String,
},
/// The client has left the current room.
///
/// The `index` field is the index of the client in the room.
Leave {
/// The index of the client in the room.
index: usize,
},
/// There was an error.
///
/// The `message` field is the error message.
Error {
/// The error message.
message: String,
},
}

View file

@ -3,17 +3,43 @@ use futures_util::stream::SplitSink;
use std::sync::Arc;
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>>>;
/// 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)]
pub struct Room {
/// The list of WebSocket senders.
///
/// Each sender is used to send messages to a WebSocket connection.
pub senders: Vec<Sender>,
/// The size of the room.
///
/// This represents the maximum number of clients allowed in the room.
pub size: usize,
}
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;
/// 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 {
Room {
senders: Vec::new(),

View file

@ -21,29 +21,45 @@ use crate::relay::client::Client;
use crate::relay::transfer::TransferResponse;
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) {
let app_host = listen_addr;
let app_port = port;
debug!("Server configured to accept connections on host {app_host}...",);
debug!("Server configured to listen connections on port {app_port}...",);
// Log the server configuration.
debug!("Server configured to accept connections on host {listen_addr}...");
debug!("Server configured to listen connections on port {port}...");
// Create a new instance of the server state.
let server = AppState::new();
// Set up the routes for the server.
let app = Router::new()
.route("/ws", get(ws_handler))
.route("/upload", put(upload_info))
.route("/download/:name", get(download_info))
.route("/download_success/:name", post(download_success))
.with_state(server)
// Set up the tracing layer to log incoming requests.
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::default().include_headers(true)),
);
if let Ok(listener) = TcpListener::bind(&format!("{}:{}", app_host, app_port)).await {
info!("Listening on: {}", listener.local_addr().unwrap());
// Start listening for incoming connections.
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(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
@ -52,35 +68,68 @@ pub async fn start_ws(port: &i32, listen_addr: &String) {
.await
.unwrap();
} 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(
ws: WebSocketUpgrade,
State(shared_state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse {
// Log the request on the WebSocket route.
debug!("Got Request on Websocket route");
// Log the connection upgrade.
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))
}
/// 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>>) {
// Split the WebSocket into a sender and receiver.
let (sender, mut receiver) = socket.split();
// Create a new Mutex to prevent concurrent access to the sender.
let sender = Arc::new(Mutex::new(sender));
// Create a new client with the sender.
let mut client = Client::new(sender.clone());
// Handle the messages received from the client.
while let Some(message) = receiver.next().await {
match message {
Ok(message) => {
// Handle the message received from the client.
client.handle_message(&rooms, message).await;
}
Err(error) => {
// Log the error if failed to read message from the client.
warn!("Failed to read message from client: {}", error);
break;
}
}
}
// Handle the close event from the client.
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(
State(shared_state): State<Arc<RwLock<AppState>>>,
Json(payload): Json<TransferRequest>,
) -> impl IntoResponse {
let mut data = shared_state.write().await;
// Find the transfer request in the shared state
match data
.transfers
.iter_mut()
.find(|request| request.name == payload.name)
{
// Update the relay_room_id or local_room_id if the request is found
Some(request) => {
debug!("Found Transfer");
debug!("Request is: {:?}", request);
@ -134,7 +201,9 @@ pub async fn upload_info(
(StatusCode::OK, Json(request.clone()))
}
}
// Create a new transfer request if the request is not found
None => {
// Initialize relay and local room IDs based on the payload
let mut local = String::from("");
let mut relay = String::from("");
if payload.relay_room_id.is_empty() {
@ -142,37 +211,58 @@ pub async fn upload_info(
} else {
relay = payload.relay_room_id;
}
// Create a new transfer request
let t_request = TransferResponse {
name: payload.name,
ip: payload.ip,
local_room_id: local,
relay_room_id: relay,
};
// Add the transfer request to the shared state
data.transfers.push(t_request.clone());
debug!("New TransferRequest created");
debug!("Actual AppState is {:#?}", *data);
// Return the created transfer request as the response
(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(
State(shared_state): State<Arc<RwLock<AppState>>>,
Path(name): Path<String>,
) -> impl IntoResponse {
// Acquire write lock on shared state
let data = shared_state.write().await;
// Find transfer request by name
match data.transfers.iter().find(|request| request.name == name) {
// If transfer request is found, return Ok response with the transfer request
Some(request) => {
debug!("Found transfer name.");
(StatusCode::OK, Json(request.clone()))
}
// If transfer request is not found, return not found response
None => {
warn!("couldn't find transfer-name: {}", name);
(
StatusCode::NOT_FOUND,
Json(TransferResponse {
// Create a new empty transfer response
name: String::from(""),
ip: 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(
State(shared_state): State<Arc<RwLock<AppState>>>,
Path(name): Path<String>,
) -> impl IntoResponse {
let mut data = shared_state.write().await;
// Find the index of the transfer request
if let Some(index) = data
.transfers
.iter()
.position(|request| request.name == name)
{
// If the transfer request is found, remove it from the shared state
debug!("Found Transfer by name '{name}'");
data.transfers.remove(index);
debug!("Transfer deleted");
// Return a success response
(
StatusCode::OK,
Json(json!({
@ -203,6 +308,7 @@ pub async fn download_success(
})),
)
} else {
// If the transfer request is not found, return a not found response
warn!("couldn't find transfer-name: {}", name);
(
StatusCode::NOT_FOUND,

View file

@ -1,13 +1,31 @@
use serde::{Deserialize, Serialize};
/// Request to transfer a connection from one relay to another
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransferRequest {
/// The name of the client
pub name: String,
/// The IP address of the client
pub ip: String,
/// The local room ID of the client
pub local_room_id: String,
/// The relay room ID of the client
pub relay_room_id: String,
}
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 {
Self {
name,
@ -18,15 +36,32 @@ impl TransferRequest {
}
}
/// Response containing the details of the transferred connection
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransferResponse {
/// The name of the client
pub name: String,
/// The IP address of the client
pub ip: String,
/// The local room ID of the client
pub local_room_id: String,
/// The relay room ID of the client
pub relay_room_id: String,
}
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 {
Self {
name,

View file

@ -31,22 +31,57 @@ const NONCE_SIZE: usize = 12;
const MAX_CHUNK_SIZE: isize = u16::MAX as isize;
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)]
struct File {
/// The path of the file to be sent.
path: String,
/// The name of the file to be sent.
name: String,
/// The size of the file to be sent.
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 {
/// The HMAC used for authentication.
hmac: Vec<u8>,
/// The sender used to send packets.
sender: Sender,
/// The ephemeral secret used for key exchange.
key: EphemeralSecret,
/// The list of files to be sent.
files: Vec<File>,
/// The shared key used for encryption.
shared_key: Option<Aes128Gcm>,
/// The task handling the sending of the files.
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(
context: &Context,
id: String,
@ -54,16 +89,26 @@ fn on_create_room(
transfer_name: String,
is_local: bool,
) -> Status {
// Debug log the relay URL
debug!("Creating room on: {relay}");
// Encode the HMAC key using base64
let base64 = general_purpose::STANDARD.encode(&context.hmac);
// Generate the URL for the room
let url = format!("{}-{}", id, base64);
// Hash the transfer name
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 h_name = hash_name.to_string();
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 || {
// Create a new runtime and block on the current thread
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@ -72,14 +117,20 @@ fn on_create_room(
})
.join()
.unwrap();
// Debug log the result
debug!("Got Result: {:?}", res);
// Handle the result of sending the information to the server
match res {
Ok(transfer_response) => {
// Print the room URL and transfer name
if !transfer_response.local_room_id.is_empty()
&& !transfer_response.relay_room_id.is_empty()
{
println!();
// Print the QR code for the transfer name
if let Err(error) = qr2term::print_qr(&transfer_name) {
error!("Failed to generate QR code: {}", error);
}
@ -90,30 +141,56 @@ fn on_create_room(
}
}
Err(e) => {
// Log the error
error!("Error sending info: {e}");
}
}
// Continue with the operation
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 {
// Check if the size of the room is provided
if size.is_some() {
return Status::Err("Invalid join room packet.".into());
}
// Generate the public key
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();
mac.update(&public_key);
let signature = mac.finalize().into_bytes().to_vec();
// Create the handshake packet with the public key and signature
let handshake = HandshakePacket {
public_key,
signature,
};
// Send the handshake packet to the receiver
context
.sender
.send_packet(DESTINATION, Value::Handshake(handshake));
@ -121,106 +198,215 @@ fn on_join_room(context: &Context, size: Option<usize>) -> Status {
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 {
// Return a `Status` with the error 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 {
// Abort any ongoing task
if let Some(task) = &context.task {
task.abort();
}
// Generate a new random key
context.key = EphemeralSecret::random(&mut OsRng);
// Clear the shared key
context.shared_key = None;
// Clear the task
context.task = None;
// Print an error message
println!();
error!("Transfer was interrupted because the receiver disconnected.");
// Return a `Status` to indicate that the operation was successful
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 {
// Check if the shared key is established
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());
}
// Get the file corresponding to the progress packet index
let file = match context.files.get(progress.index as usize) {
Some(file) => file,
None => return Status::Err("Invalid index in progress packet.".into()),
};
// Print the progress of the file transfer
print!("\rTransferring '{}': {}%", file.name, progress.progress);
// Flush the stdout
stdout().flush().unwrap();
// Check if the progress is 100%
if progress.progress == 100 {
// Print a newline
println!();
// Check if this is the last file being transferred
if progress.index as usize == context.files.len() - 1 {
// Return an exit status to indicate that the operation was successful
return Status::Exit();
}
}
// Return a continue status to indicate that the operation was successful
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 {
let mut sequence = 0;
let mut chunk_size = MAX_CHUNK_SIZE;
let mut size = file.size as isize;
// Open the file
let mut handle = match tokio::fs::File::open(file.path).await {
Ok(handle) => handle,
Err(error) => {
// Print an error message if the file cannot be opened
println!("Error: Unable to open file '{}': {}", file.name, error);
return;
}
};
// While there are still chunks to be transferred
while size > 0 {
// If the remaining size is less than the maximum chunk size
if size < chunk_size {
// Set the chunk size to the remaining size
chunk_size = size;
}
// Create a vector to hold the chunk
let mut chunk = vec![0u8; chunk_size.try_into().unwrap()];
// Read the chunk from the file
handle.read_exact(&mut chunk).await.unwrap();
// Send the encrypted chunk packet to the receiver
sender.send_encrypted_packet(
&shared_key,
DESTINATION,
Value::Chunk(ChunkPacket { sequence, chunk }),
);
// Increment the sequence and decrement the size
sequence += 1;
size -= chunk_size;
}
// Wait for a delay before starting the next file transfer
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 {
// Create a vector of `Entry`s from the files in the context
let mut entries = vec![];
for (index, file) in context.files.iter().enumerate() {
let entry = list_packet::Entry {
// The index of the file in the context
index: index.try_into().unwrap(),
// The name of the file
name: file.name.clone(),
// The size of the file
size: file.size,
};
entries.push(entry);
}
// Send the encrypted list packet to the receiver
context.sender.send_encrypted_packet(
&context.shared_key,
DESTINATION,
Value::List(ListPacket { entries }),
);
// Spawn the file transfer task and store it in the context
context.task = Some(tokio::spawn(on_chunk(
context.sender.clone(),
context.shared_key.clone(),
@ -230,34 +416,73 @@ fn on_handshake_finalize(context: &mut Context) -> Status {
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 {
// Check if the handshake has already been performed
if context.shared_key.is_some() {
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();
// Update the HMAC with the public key from the handshake response
mac.update(&handshake_response.public_key);
// Verify the signature in the handshake response
let verification = mac.verify_slice(&handshake_response.signature);
if verification.is_err() {
// Return an error if the signature is invalid
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();
// Derive the shared secret using Diffie-Hellman key exchange
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();
// Take the first 16 bytes of the secret as the shared key
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();
// Create a new instance of `Aes128Gcm` with the 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);
// Finalize the handshake by sending the list of files and starting the file transfer task
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(
context: &mut Context,
message: WebSocketMessage,
@ -266,25 +491,36 @@ fn on_message(
is_local: bool,
) -> Status {
match message.clone() {
// Handle the text WebSocket message
WebSocketMessage::Text(text) => {
// Parse the JSON packet from the text message
let packet = match serde_json::from_str(&text) {
Ok(packet) => packet,
Err(_) => {
return Status::Continue();
}
};
// Call the corresponding handler based on the packet type
return match packet {
// Handle the `Create` packet
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),
// Handle the `Leave` packet
JsonPacketResponse::Leave { index } => on_leave_room(context, index),
// Handle the `Error` packet
JsonPacketResponse::Error { message } => on_error(message),
};
}
// Handle the binary WebSocket message
WebSocketMessage::Binary(data) => {
// Extract the encrypted data from the binary message
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 nonce = &data[..NONCE_SIZE];
let ciphertext = &data[NONCE_SIZE..];
@ -294,23 +530,41 @@ fn on_message(
data
};
// Decode the packet from the decrypted data
let packet = Packet::decode(data.as_ref()).unwrap();
let value = packet.value.unwrap();
// Call the corresponding handler based on the packet value
return match value {
// Handle the `HandshakeResponse` packet
Value::HandshakeResponse(handshake_response) => {
on_handshake(context, handshake_response)
}
// Handle the `Progress` packet
Value::Progress(progress) => on_progress(context, progress),
// Handle unexpected packets
_ => Status::Err(format!("Unexpected packet: {:?}", value)),
}
};
}
// Handle other message types
_ => (),
}
// Return an error for unsupported message types
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(
socket: Socket,
paths: Vec<String>,
@ -319,9 +573,11 @@ pub async fn start(
transfer_name: String,
is_local: bool,
) {
// Prepare the files to be sent
let mut files = vec![];
for path in paths {
// Open the file
let handle = match fs::File::open(&path) {
Ok(handle) => handle,
Err(error) => {
@ -332,6 +588,7 @@ pub async fn start(
let metadata = handle.metadata().unwrap();
// Check if the path points to a file
if metadata.is_dir() {
error!("Error: The path '{}' does not point to a file.", path);
return;
@ -341,6 +598,7 @@ pub async fn start(
let size = metadata.len();
// Check if the file is empty
if size == 0 {
error!("Error: The file '{}' is empty and cannot be sent.", name);
return;
@ -353,15 +611,20 @@ pub async fn start(
});
}
// Generate the HMAC key
let mut hmac = [0u8; 32];
OsRng.fill_bytes(&mut hmac);
// Generate the encryption key
let key = EphemeralSecret::random(&mut OsRng);
// Create the flume channels
let (sender, receiver) = flume::bounded(1000);
// Split the WebSocket connection
let (outgoing, incoming) = socket.split();
// Create the context
let mut context = Context {
sender,
key,
@ -375,10 +638,12 @@ pub async fn start(
debug!("Attempting to create room...");
debug!("With Room-ID: {:?}", room_id);
// Send the create room packet
context.sender.send_json_packet(JsonPacket::Create {
id: room_id.clone(),
});
// Handle the incoming WebSocket messages
let outgoing_handler = receiver.stream().map(Ok).forward(outgoing);
let incoming_handler = incoming.try_for_each(|message| {
@ -390,15 +655,17 @@ pub async fn start(
is_local,
) {
Status::Exit() => {
// TODO: Signal Exit to the server
// Send the leave room packet
context.sender.send_json_packet(JsonPacket::Leave);
println!("Transfer has completed.");
// Return an error
return future::err(Error::ConnectionClosed);
}
Status::Err(error) => {
error!("Error: {}", error);
// Return an error
return future::err(Error::ConnectionClosed);
}
_ => {}
@ -409,6 +676,7 @@ pub async fn start(
pin_mut!(incoming_handler, outgoing_handler);
// Wait for the incoming or outgoing handlers to complete
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>>;
/// 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(
relay: &str,
name: &str,
room_id: &str,
is_local: bool,
) -> Result<TransferResponse> {
// Build the URL for the request
let url = relay.to_string();
// Get the sender's IP address
let sender_ip = match local_ipv6() {
Ok(ip) => ip,
Err(_) => match local_ip() {
Ok(ip) => ip,
Err(e) => {
// Log the error and return the error
error!("Error getting local ip: {e:?}");
return Err(Box::new(e));
}
@ -27,14 +43,19 @@ pub async fn send_info(
};
let ip_str = sender_ip.to_owned().to_string();
// Create the transfer request
let transfer_request = TransferRequest {
// Set the name of the sender
name: String::from(name),
// Set the IP address of the sender
ip: ip_str,
// Set the room ID for the local sender
local_room_id: if is_local {
String::from(room_id)
} else {
String::from("")
},
// Set the room ID for the relay sender
relay_room_id: if !is_local {
String::from(room_id)
} else {
@ -42,7 +63,10 @@ pub async fn send_info(
},
};
// Log the start of the request
debug!("Trying to send Request.");
// Send the request and parse the response
let result: Result<TransferResponse> = task::spawn_blocking(move || {
let client = Client::new();
let response = client
@ -54,5 +78,6 @@ pub async fn send_info(
})
.await?;
// Return the result
result
}

View file

@ -6,7 +6,7 @@ use std::{net::SocketAddr, sync::Arc};
use crate::{
relay::{appstate::AppState, server::ws_handler},
sender::{client as sender, util::generate_random_name},
sender::client as sender,
};
use axum::{routing::get, Router};
use tokio::{net::TcpListener, sync::mpsc, task};
@ -18,39 +18,49 @@ use tower_http::trace::{DefaultMakeSpan, TraceLayer};
use tracing::{debug, error, info};
use uuid::Uuid;
pub async fn start_sender(relay: Arc<String>, files: Arc<Vec<String>>, transfer_name: String) {
/// 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>>) {
// Log the name of the sender
debug!("Got name: {:?}", name);
// Create a channel for communication between threads
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 rand_name: String;
if transfer_name.is_empty() {
rand_name = generate_random_name();
} else {
rand_name = transfer_name.clone();
}
let local_room_id = room_id.clone();
let local_files = files.clone();
let local_relay = relay.clone();
let local_rand_name = rand_name.clone();
let local_rand_name = name.clone();
let local_tx = tx.clone();
// Start a local WebSocket server
let local_ws_thread = task::spawn(async move {
start_local_ws().await;
});
// Connect to the relay
let relay_thread = task::spawn(async move {
connect_to_server(
relay.clone(),
files.clone(),
Some(room_id),
relay.clone(),
Arc::new(rand_name.clone()),
Arc::new(name.clone()),
tx.clone(),
false,
)
.await
});
// Connect to the local WebSocket server
let local_thread = task::spawn(async move {
connect_to_server(
Arc::new(String::from("ws://localhost:9000")),
Arc::new(String::from("ws://0.0.0.0:9000")),
local_files.clone(),
Some(local_room_id),
local_relay.clone(),
@ -61,19 +71,40 @@ pub async fn start_sender(relay: Arc<String>, files: Arc<Vec<String>>, transfer_
.await
});
// Wait for the sender threads to finish
rx.recv().await.unwrap();
// Abort the local WebSocket server thread
local_ws_thread.abort();
// Abort the relay thread
relay_thread.abort();
// Abort the local thread
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() {
let app_host = "localhost";
// The host and port the server will listen on.
let app_host = "0.0.0.0";
let app_port = "9000";
// Create an instance of the application state.
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()
.route("/ws", get(ws_handler))
.with_state(server)
@ -82,12 +113,16 @@ pub async fn start_local_ws() {
.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 {
// Log the address the server is listening on.
info!(
"Local Websocket listening on: {}",
"Local WebSocket listening on: {}",
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(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
@ -95,10 +130,22 @@ pub async fn start_local_ws() {
.await
.unwrap();
} else {
// Log an error if the server fails to bind.
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(
relay: Arc<String>,
files: Arc<Vec<String>>,
@ -108,21 +155,33 @@ async fn connect_to_server(
tx: mpsc::Sender<()>,
is_local: bool,
) {
// Construct the server URL.
let url = format!("{}/ws", relay);
// Construct the message server URL.
let message_relay = format!("{}", message_server);
// Construct the transfer name.
let transfer_name = format!("{}", transfer_name);
// Create a request to the server.
match url.clone().into_client_request() {
Ok(mut request) => {
// Set the "Origin" header.
request
.headers_mut()
.insert("Origin", HeaderValue::from_str(relay.as_ref()).unwrap());
// Log the connection attempt.
debug!("Attempting to connect to {url}...");
// Generate a room ID if not provided.
let room_id = match room_id {
Some(id) => id,
None => Uuid::new_v4().to_string(),
};
// Connect to the server and start the file transfer.
match connect_async(request).await {
Ok((socket, _)) => {
let paths = files.to_vec();
@ -135,14 +194,18 @@ async fn connect_to_server(
is_local,
)
.await;
// Signal the completion of the transfer.
tx.send(()).await.unwrap();
}
Err(e) => {
// Log the connection error.
error!("Error: Failed to connect with error: {e}");
}
}
}
Err(e) => {
// Log the request creation error.
error!("Error: failed to create request with reason: {e:?}");
}
}

View file

@ -2,40 +2,79 @@ use hex;
use rand::{seq::SliceRandom, thread_rng};
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 {
let mut rng = thread_rng();
let adjective = adjectives().choose(&mut rng).unwrap();
// let adjective = adjectives().sample(&mut rng).unwrap();
let noun1 = nouns1().choose(&mut rng).unwrap();
let noun2 = nouns2().choose(&mut rng).unwrap();
format!("{adjective}-{noun1}-{noun2}")
}
/// Returns a random adjective.
///
/// # Returns
///
/// A `&'static str` representing an adjective.
fn adjectives() -> &'static [&'static str] {
// Define a list of adjectives.
static ADJECTIVES: &[&str] = &["funny", "smart", "creative", "friendly", "great"];
ADJECTIVES
}
/// Returns a random noun.
///
/// # Returns
///
/// A `&'static str` representing a noun.
fn nouns1() -> &'static [&'static str] {
// Define a list of nouns.
static NOUNS1: &[&str] = &["dog", "cat", "flower", "tree", "house"];
NOUNS1
}
/// Returns a random noun.
///
/// # Returns
///
/// A `&'static str` representing a noun.
fn nouns2() -> &'static [&'static str] {
// Define a list of nouns.
static NOUNS2: &[&str] = &["cookie", "cake", "frosting"];
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 {
let hashed_name = Sha256::digest(name.as_bytes());
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 {
let mut result = address.to_string();
result = result.replace("ws://", "http://");
result = result.replace("wss://", "https://");
result
@ -51,6 +90,26 @@ mod tests {
assert!(name.contains('-'));
assert!(name.split('-').count() == 3);
assert!(name.is_empty());
// assert!(name.is_empty());
}
#[test]
fn test_replace_protocol_replaces_ws_with_http() {
let result = replace_protocol("ws://example.com");
assert_eq!(result, "http://example.com");
}
#[test]
fn test_replace_protocol_replaces_wss_with_https() {
let result = replace_protocol("wss://example.com");
assert_eq!(result, "https://example.com");
}
#[test]
fn test_replace_protocol_does_not_replace_http_or_https() {
let result = replace_protocol("http://example.com");
assert_eq!(result, "http://example.com");
let result = replace_protocol("https://example.com");
assert_eq!(result, "https://example.com");
}
}

View file

@ -14,46 +14,116 @@ use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::protocol::Message as WebSocketMessage;
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)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum JsonPacket {
Join { id: String },
Create { id: Option<String> },
/// A packet to join a room.
///
/// 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,
}
/// 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)]
#[serde(tag = "type", rename_all = "camelCase")]
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 {
/// The number of existing users in the room. This field is `None` if the room is empty.
#[serde(skip_serializing_if = "Option::is_none")]
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 {
/// The id of the created room.
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 {
/// The index of the user who left the room.
index: usize,
},
/// An error response.
///
/// This variant is used to indicate an error. The `message` field is used to specify the error message.
Error {
/// The error message.
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)]
pub enum Status {
/// Operation was successful and the client should continue.
Continue(),
/// Operation was successful and the client should exit.
Exit(),
/// Operation encountered an error. The error message is provided in the variant.
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 {
/// 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);
}
/// 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 {
/// 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);
/// 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(
&self,
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))
.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 {
/// 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) {
// Serialize the packet
let packet = Packet { value: Some(value) };
let mut serialized_packet = packet.encode_to_vec();
// Insert the destination at the beginning of the packet
serialized_packet.insert(0, destination);
// Send the serialized packet as a WebSocketMessage::Binary
self.send(WebSocketMessage::Binary(serialized_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(
&self,
key: &Option<Aes128Gcm>,
destination: u8,
value: packets::packet::Value,
) {
// Serialize the packet
let packet = Packet { value: Some(value) };
// Generate a nonce for encryption
let nonce = Aes128Gcm::generate_nonce(&mut OsRng);
// Serialize the packet
let plaintext = packet.encode_to_vec();
// Encrypt the packet using the provided key
let mut ciphertext = key
.as_ref()
.unwrap()
.encrypt(&nonce, plaintext.as_ref())
.expect("Failed to encrypt Packet.");
// Create the serialized packet by concatenating the nonce and the ciphertext
let mut serialized_packet = nonce.to_vec();
serialized_packet.append(&mut ciphertext);
// Insert the destination at the beginning of the packet
serialized_packet.insert(0, destination);
// Send the serialized packet as a WebSocketMessage::Binary
self.send(WebSocketMessage::Binary(serialized_packet))
.expect("Failed to send encrypted Packet.");
}

View file

@ -1,10 +0,0 @@
[package]
name = "caesar-desktop"
version = "0.0.1"
edition = "2021"
authors = ["Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
caesar-core = { path = "../caesar-core" }

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View file

@ -1,10 +0,0 @@
[package]
name = "caesar-mobile"
version = "0.0.1"
edition = "2021"
authors = ["Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
caesar-core = { path = "../caesar-core" }

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View file

@ -1,6 +1,6 @@
[package]
name = "caesar-transfer-iu"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["Manuel Keidel, Patryk Hegenberg, Krzysztof Stankiewicz"]

View file

@ -10,6 +10,18 @@ use caesar_core::relay::server::upload_info;
use caesar_core::relay::server::ws_handler;
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]
async fn axum() -> ShuttleAxum {
// Create a new server data structure.
@ -17,12 +29,13 @@ async fn axum() -> ShuttleAxum {
// Set up the application routes.
let app = Router::new()
.route("/ws", get(ws_handler))
.route("/upload", put(upload_info))
.route("/download/:name", get(download_info))
.route("/download_success/:name", post(download_success))
.with_state(appstate)
.layer(SecureClientIpSource::ConnectInfo.into_extension());
.route("/ws", get(ws_handler)) // Route for the websocket handler
.route("/upload", put(upload_info)) // Route for the upload info handler
.route("/download/:name", get(download_info)) // Route for the download info handler
.route("/download_success/:name", post(download_success)) // Route for the download success handler
.with_state(appstate) // Wrap the routes with the application state
.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())
}

View file

@ -1,42 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View file

@ -1,7 +0,0 @@
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"angular.ng-template"
]
}

View file

@ -1,7 +0,0 @@
# Tauri + Angular
This template should help get you started developing with Tauri and Angular.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template).

View file

@ -1,68 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"cli": {
"analytics": false
},
"projects": {
"caesar-tauri-angular": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/caesar-tauri-angular",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/assets"],
"styles": ["src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"port": 1420
},
"configurations": {
"production": {
"buildTarget": "caesar-tauri-angular:build:production"
},
"development": {
"buildTarget": "caesar-tauri-angular:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,44 +0,0 @@
{
"name": "caesar-tauri-angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"tauri": "tauri"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-dialog": "^2.0.0-beta.3",
"@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
"angularx-qrcode": "^17.0.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/jasmine": "~5.1.0",
"internal-ip": "^7.0.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2"
}
}

View file

@ -1,4 +0,0 @@
[build]
target = "x86_64-pc-windows-msvc"
[target]

View file

@ -1,7 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View file

@ -1,26 +0,0 @@
[package]
name = "caesar-tauri-angular"
version = "0.0.0"
description = "A Tauri App"
authors = ["Manuel Keidel"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "caesar_tauri_angular_lib"
crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
log = "0.4"
env_logger = "0.11.3"
caesar-core = { path = "../../caesar-core" }
tauri = { version = "2.0.0-beta", features = [] }
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View file

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View file

@ -1,18 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"window:allow-set-title",
"dialog:allow-open",
"dialog:allow-message"
]
}

View file

@ -1,19 +0,0 @@
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "mobile-capability",
"windows": ["main"],
"platforms": ["iOS", "android"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"window:allow-set-title",
"dialog:allow-open",
"dialog:allow-message",
"menu:allow-get"
]
}

View file

@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View file

@ -1,19 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
/.tauri
/tauri.settings.gradle

View file

@ -1,5 +0,0 @@
/src/main/java/com/tauri/caesar_tauri_angular/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro

View file

@ -1,57 +0,0 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
android {
compileSdk = 33
namespace = "com.tauri.caesar_tauri_angular"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "true"
applicationId = "com.tauri.caesar_tauri_angular"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.caesar_tauri_angular"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View file

@ -1,3 +0,0 @@
package com.tauri.caesar_tauri_angular
class MainActivity : TauriActivity()

View file

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.caesar_tauri_angular" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -1,4 +0,0 @@
<resources>
<string name="app_name">caesar-tauri-angular</string>
<string name="main_activity_title">caesar-tauri-angular</string>
</resources>

View file

@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.caesar_tauri_angular" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>

View file

@ -1,22 +0,0 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.0.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}

View file

@ -1,23 +0,0 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.0.0")
}

View file

@ -1,52 +0,0 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """npm""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
runTauriCli("$executable.cmd")
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}

View file

@ -1,85 +0,0 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}

View file

@ -1,25 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false

View file

@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View file

@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -1,3 +0,0 @@
include ':app'
apply from: 'tauri.settings.gradle'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,52 +0,0 @@
use caesar_core::{receiver, relay::server::start_ws, sender::{self, util::generate_random_name}};
use std::sync::Arc;
use tauri::{AppHandle, Manager};
#[tauri::command]
async fn send(app_handle: tauri::AppHandle, relay: Option<String>, files: Vec<String>) {
let relay_string = relay.unwrap_or_else(|| "default_relay_address".to_string());
log::info!("Using relay: {}", relay_string);
let relay_arc = Arc::new(relay_string);
let files_arc = Arc::new(files);
let transfer_name = generate_random_name();
app_handle.emit("transfer_name_event", transfer_name.clone())
.expect("Failed to emit event");
sender::start_sender(relay_arc, files_arc, transfer_name.clone()).await;
}
#[tauri::command]
async fn receive(relay: Option<String>, name: String) -> Result<(), String> {
let relay_string = relay.unwrap_or_else(|| "default_relay_address".to_string());
match receiver::start_receiver(&relay_string, &name).await {
Ok(_) => {
println!("Receiver started successfully.");
Ok(())
}
Err(e) => {
eprintln!("Failed to start receiver: {:?}", e);
Err(format!("Failed to start receiver: {:?}", e))
}
}
}
#[tauri::command]
async fn serve(port: Option<i32>, listen_address: Option<String>) {
let address = listen_address.unwrap_or_else(|| "localhost".to_string());
let port = port.unwrap_or(8080);
start_ws(&port, &address).await;
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
env_logger::init();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![send, serve, receive])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
caesar_tauri_angular_lib::run()
}

View file

@ -1,35 +0,0 @@
{
"productName": "caesar-tauri-angular",
"version": "0.0.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "npm run start -- --host $HOST --public-host $HOST",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist/caesar-tauri-angular/browser"
},
"app": {"windows": [
{
"title": "caesar-tauri-angular",
"width": 800,
"height": 600
}
],
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;",
"capabilities": ["main-capability", "mobile-capability"]
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

Some files were not shown because too many files have changed in this diff Show more