Compare commits
27 commits
101-implem
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e3c6cbd231 | |||
|
|
7829b76080 | ||
|
|
b6329eb698 | ||
|
|
62f84255e3 | ||
|
|
d5cf4d86f2 | ||
|
|
f19648a386 | ||
| 3abff684ad | |||
|
|
f76f89bf9f | ||
| 0465851ec5 | |||
| 07584a48a1 | |||
| 4a3b1a7996 | |||
| 0d354efb8e | |||
| 47acdec22a | |||
|
|
4697cf35db | ||
|
|
fc5d8efed6 | ||
| 4af2255d2f | |||
| cd43fd6041 | |||
| d9501024db | |||
| 0416ab6dab | |||
| 351b7b9323 | |||
| 1586818bd6 | |||
| 1b83c0b5ff | |||
| f0f67332a0 | |||
| fbb212f383 | |||
| fbb181d95b | |||
| f1e48f6d12 | |||
| 113e1cfe53 |
26
.github/workflows/build_and_test_on_push.yml
vendored
Normal 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
10
Cargo.toml
|
|
@ -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
|
|
@ -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.
|
||||
35
README.md
|
|
@ -1,22 +1,28 @@
|
|||
# Caesar
|
||||
This program provides a simple relay server that can be controlled via the command line.
|
||||
# Caesar-Transfer
|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
42
caesar-tauri-angular/.gitignore
vendored
|
|
@ -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
|
||||
7
caesar-tauri-angular/.vscode/extensions.json
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"tauri-apps.tauri-vscode",
|
||||
"rust-lang.rust-analyzer",
|
||||
"angular.ng-template"
|
||||
]
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12905
caesar-tauri-angular/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[build]
|
||||
target = "x86_64-pc-windows-msvc"
|
||||
|
||||
[target]
|
||||
7
caesar-tauri-angular/src-tauri/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tauri.caesar_tauri_angular
|
||||
|
||||
class MainActivity : TauriActivity()
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">caesar-tauri-angular</string>
|
||||
<string name="main_activity_title">caesar-tauri-angular</string>
|
||||
</resources>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
185
caesar-tauri-angular/src-tauri/gen/android/gradlew
vendored
|
|
@ -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" "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 974 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||