chore: commit history pruned

This commit is contained in:
ae 2024-09-24 00:14:53 +03:00
commit 306eb86d74
19 changed files with 2612 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/target
tests/data/**
tests/output/**
!tests/output/.placeholder
!tests/data/.placeholder

8
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,8 @@
repos:
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: fmt
args: ["--verbose", "--"]
- id: cargo-check
- id: clippy

1499
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "contego"
version = "0.4.0"
authors = ["ae"]
description = "Cryptographically secure file transfer CLI tool "
repository = "https://umbrella.haus/ae/contego"
license = "MIT"
edition = "2021"
[dependencies]
tokio = { version = "1.28.1", features = ["full"] }
rand = "0.7.0"
x25519-dalek = "1.2.0"
aes-gcm = "0.10.3"
base64 = "0.21.2"
sha256 = "1.1.3"
ureq = "2.6.2"
clap = { version = "4.3.0", features = ["derive"] }
log = "0.4.17"
env_logger = "0.10.0"
[dev-dependencies]
tokio-test = "0.4.2"
ntest = "0.9.0"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 ae
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.

43
README.md Normal file
View File

@ -0,0 +1,43 @@
<div align="center" style="text-align:center">
<picture>
<source media="(prefers-color-scheme: light)" srcset="docs/contego-light.png">
<img src="docs/contego-dark.png" width="800">
</picture>
</div>
## Cryptographic specifications
The initial key exchange is performed with elliptic-curve Diffie-Hellman. General data exchange is encrypted with AES-GCM. During regular communication payloads are Base64 encoded before being encrypted to prevent delimiter conflicts. SHA-256 hashes of files are compared to ensure data integrity.
## Usage
Build the optimized binary with `cargo build --release`.
### Server
```
Usage: contego host [OPTIONS] --key <KEY> <--source <SOURCE>|--files <FILES>...>
Options:
-k, --key <KEY> Access key
-s, --source <SOURCE> Path to a source file (alternative to --files)
-f, --files <FILES>... Paths to shareable files (alternative to --source)
-p, --port <PORT> Host port [default: 8080]
-6, --ipv6 IPv6 instead of IPv4
-c, --chunksize <CHUNKSIZE> Transmit chunksize in bytes [default: 8192]
-l, --local Host locally
-h, --help Print help
```
### Client
```
Usage: contego connect --addr <ADDR> --out <OUT> --key <KEY>
Options:
-a, --addr <ADDR> IP address of the instance
-o, --out <OUT> Path to an output folder
-k, --key <KEY> Access key
-h, --help Print help
```

BIN
docs/contego-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/contego-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

148
src/client.rs Normal file
View File

@ -0,0 +1,148 @@
use std::{error::Error, net::SocketAddr, path::PathBuf};
use log::{debug, error, info};
use tokio::{io::AsyncWriteExt, net::TcpStream};
use crate::{
crypto::{self, Crypto},
sockets::SocketHandler,
util::{new_file, FileInfo},
};
#[derive(Clone)]
pub struct Client {
addr: SocketAddr,
key: String,
output: PathBuf,
}
impl Client {
pub fn new(addr: SocketAddr, key: String, output: PathBuf) -> Self {
Self { addr, key, output }
}
pub async fn connection(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
info!("Trying to connect to the server at {}", self.addr);
let mut socket = TcpStream::connect(self.addr).await?;
debug!("Connected to the TCP socket at {}", self.addr);
let mut handler = SocketHandler::new(&mut socket);
let crypto = Crypto::new(&mut handler, true).await?;
handler.set_crypto(crypto);
info!("Encrypted connection to {} established", self.addr);
if !self.authorize(&mut handler).await? {
error!(
"Authorization failed due to an invalid access key '{}'",
self.key
);
return Ok(());
}
let metadata = self.metadata(&mut handler).await?;
self.requests(&mut handler, metadata).await?;
debug!("Connection sequence done, shutting down");
Ok(())
}
async fn authorize(
&self,
handler: &mut SocketHandler<'_>,
) -> Result<bool, Box<dyn Error + Send + Sync>> {
debug!("Starting authorization");
let msg = self.key.as_bytes().to_vec();
handler.send(&msg).await?;
let buf = handler.recv().await?;
let msg = String::from_utf8(buf)?;
let msg = msg.trim();
if msg == "DISCONNECT" {
return Ok(false);
}
debug!("Authorization successfully done");
Ok(true)
}
async fn metadata(
&self,
handler: &mut SocketHandler<'_>,
) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
debug!("Starting to receive metadata");
let buf = handler.recv().await?;
let amt = String::from_utf8(buf.clone())?.parse::<usize>()?;
handler.send(&buf).await?; // confirmation
debug!("Confirmed metadata amount ({})", amt);
let mut metadata = Vec::new();
while metadata.len() < amt {
let buf = handler.recv().await?;
let data = String::from_utf8(buf)?;
let split = data.split(':').collect::<Vec<&str>>();
let name = split[0].trim().to_string();
let size = split[1].trim().parse::<u64>()?;
let hash = split[2].trim().to_string();
debug!("Metadata of file '{}' received successfully", name);
let info = FileInfo::new(name, size, hash);
metadata.push(info);
}
Ok(metadata)
}
async fn requests(
&self,
handler: &mut SocketHandler<'_>,
metadata: Vec<FileInfo>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
info!("Starting to send requests");
for file in metadata {
let (mut handle, path) = new_file(self.output.clone(), &file.name).await?;
let msg = file.hash.as_bytes().to_vec();
handler.send(&msg).await?;
info!("Requesting file '{}'", file.hash);
let mut remaining = file.size;
while remaining != 0 {
let buf = handler.recv().await?;
handle.write_all(&buf).await?;
handle.flush().await?;
remaining -= buf.len() as u64;
debug!("File '{}': {} bytes remaining", file.hash, remaining);
}
let check_hash = crypto::try_hash(&path).unwrap();
let msg = check_hash.as_bytes().to_vec();
handler.send(&msg).await?;
if check_hash != file.hash {
return Err("Unsuccessful file transfer, hashes don't match".into());
}
info!("File '{}' successfully transferred", file.hash);
}
info!("All requests successfully done");
Ok(())
}
}

111
src/crypto.rs Normal file
View File

@ -0,0 +1,111 @@
use std::{error::Error, path::Path};
use aes_gcm::{
aead::{consts::U12, Aead},
aes::Aes256,
Aes256Gcm, AesGcm, KeyInit, Nonce,
};
use log::debug;
use rand::{rngs::OsRng, RngCore};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
use crate::sockets::SocketHandler;
const AES_NONCE_SIZE: usize = 12;
const DH_PBK_SIZE: usize = 32;
#[derive(Clone)]
pub struct Crypto {
cipher: AesGcm<Aes256, U12>,
rng: OsRng,
}
impl Crypto {
pub async fn new(
handler: &mut SocketHandler<'_>,
go_first: bool,
) -> Result<Self, Box<dyn Error + Sync + Send>> {
let secret = Self::ecdh(handler, go_first).await?;
let cipher = Aes256Gcm::new(secret.as_bytes().into());
let rng = OsRng;
Ok(Self { cipher, rng })
}
async fn ecdh(
handler: &mut SocketHandler<'_>,
go_first: bool,
) -> Result<SharedSecret, Box<dyn Error + Send + Sync>> {
debug!("Starting ECDH key exchange");
let buf: Vec<u8>;
let own_sec = EphemeralSecret::new(OsRng);
let own_pbk = PublicKey::from(&own_sec);
let mut msg = own_pbk.as_bytes().to_vec();
msg.push(b':'); // manual delimiter
if go_first {
handler.send_raw(&msg).await?;
buf = handler.recv_raw(DH_PBK_SIZE).await?;
} else {
buf = handler.recv_raw(DH_PBK_SIZE).await?;
handler.send_raw(&msg).await?;
}
debug!("Calculating PPK from the shared secret");
let slice: [u8; DH_PBK_SIZE] = buf[..DH_PBK_SIZE].try_into()?;
let recv_pbk = PublicKey::from(slice);
let pvk = own_sec.diffie_hellman(&recv_pbk);
debug!("PPK successfully generated");
Ok(pvk)
}
fn nonce(&mut self) -> Nonce<U12> {
debug!("Generating new unique nonce (AEAD)");
let mut nonce = Nonce::default();
self.rng.fill_bytes(&mut nonce);
nonce
}
pub async fn encrypt(&mut self, data: &[u8]) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
debug!("Encrypting {} bytes payload", data.len());
let nonce = self.nonce();
let encrypted = match self.cipher.encrypt(&nonce, data.as_ref()) {
Ok(data) => data,
Err(e) => return Err(format!("Encryption failed: {}", e).into()),
};
let mut data = nonce.to_vec();
data.extend_from_slice(&encrypted);
Ok(data)
}
pub async fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
debug!("Decrypting {} bytes payload", data.len());
let (nonce_bytes, data) = data.split_at(AES_NONCE_SIZE);
let nonce = Nonce::from_slice(nonce_bytes);
let decrypted = match self.cipher.decrypt(nonce, data.as_ref()) {
Ok(data) => data,
Err(e) => return Err(format!("Decryption failed: {}", e).into()),
};
Ok(decrypted)
}
}
pub fn try_hash(path: &Path) -> Result<String, Box<dyn Error>> {
debug!("Calculating SHA hash");
let hash = sha256::try_digest(path)?;
Ok(hash)
}

6
src/lib.rs Executable file
View File

@ -0,0 +1,6 @@
pub mod client;
pub mod crypto;
pub mod parser;
pub mod server;
pub mod sockets;
pub mod util;

115
src/main.rs Executable file
View File

@ -0,0 +1,115 @@
use std::{error::Error, net::SocketAddr, path::PathBuf};
use clap::{command, ArgGroup, Parser, Subcommand};
use contego::{
client::Client,
parser::{addr_parser, dirpath_parser, filepath_parser},
server::Server,
util::{ascii, filepaths, metadata, Ip},
};
use env_logger::Env;
use log::{error, info};
use tokio::{signal, sync::mpsc};
#[derive(Debug, Parser)]
#[command(about, version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
#[clap(group(ArgGroup::new("input").required(true).args(&["source", "files"])))]
Host {
/// Access key
#[clap(short = 'k', long)]
key: String,
/// Path to a source file (alternative to --files)
#[clap(short = 's', long, value_parser = filepath_parser, conflicts_with = "files", group = "input")]
source: Option<PathBuf>,
/// Paths to shareable files (alternative to --source)
#[clap(short = 'f', long, num_args = 1.., value_parser = filepath_parser, conflicts_with = "source", group = "input")]
files: Option<Vec<PathBuf>>,
/// Host port
#[clap(short = 'p', long, default_value_t = 8080)]
port: u16,
/// IPv6 instead of IPv4
#[clap(short = '6', long, default_value_t = false)]
ipv6: bool,
/// Transmit chunksize in bytes
#[clap(short = 'c', long, default_value_t = 8192)]
chunksize: usize,
/// Host locally
#[clap(short = 'l', long, default_value_t = false)]
local: bool,
},
Connect {
/// IP address of the instance
#[clap(short = 'a', long, value_parser = addr_parser)]
addr: SocketAddr,
/// Path to an output folder
#[clap(short = 'o', long, value_parser = dirpath_parser)]
out: PathBuf,
/// Access key
#[clap(short = 'k', long)]
key: String,
},
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
ascii();
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let cli = Cli::parse();
match cli.command {
Commands::Host {
port,
ipv6,
source,
files,
chunksize,
local,
key,
} => {
let (tx, rx) = mpsc::channel::<()>(1);
let paths = filepaths(source, files)?;
let (metadata, index) = metadata(&paths).await?;
let (display_addr, bind_addr) = match (local, ipv6) {
(true, _) => Ip::Local.fetch(port)?,
(false, true) => Ip::V6.fetch(port)?,
(false, false) => Ip::V4.fetch(port)?,
};
let server = Server::new(display_addr, key, chunksize, metadata, index);
tokio::spawn(async move {
match server.start(rx, &bind_addr).await {
Ok(_) => {}
Err(e) => error!("Error during server execution: {}", e),
};
});
match signal::ctrl_c().await {
Ok(_) => {
tx.send(()).await?;
info!("Captured Ctrl+C, shutting down");
}
Err(_) => error!("Failed to listen for a Ctrl+C event"),
};
}
Commands::Connect { addr, out, key } => {
let client = Client::new(addr, key, out);
match client.connection().await {
Ok(_) => {}
Err(e) => error!("Error during client execution: {}", e),
};
}
};
Ok(())
}

94
src/parser.rs Normal file
View File

@ -0,0 +1,94 @@
use std::{
env,
io::{Error, ErrorKind::NotFound},
net::{AddrParseError, SocketAddr},
path::PathBuf,
};
use log::debug;
pub fn addr_parser(addr: &str) -> Result<SocketAddr, AddrParseError> {
let addr = addr
.parse::<SocketAddr>()
.expect("Failed to parse IP address");
Ok(addr)
}
pub fn filepath_parser(path: &str) -> Result<PathBuf, Error> {
debug!("Validating filepath '{}'", path);
let home = env::var("HOME").unwrap();
let path = path
.replace('~', &home)
.parse::<PathBuf>()
.expect("Failed to parse path");
if path.exists() && path.is_file() {
Ok(path)
} else {
Err(Error::new(NotFound, "File not found"))
}
}
pub fn dirpath_parser(path: &str) -> Result<PathBuf, Error> {
debug!("Validating dirpath '{}'", path);
let path = path.parse::<PathBuf>().expect("Failed to parse path");
if path.exists() && path.is_dir() {
Ok(path)
} else {
Err(Error::new(NotFound, "Directory not found"))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn valid_ip() {
use std::net::Ipv6Addr;
let ipv4 = "10.1.2.3:8888";
let ipv6 = "[2001:db8::1]:8888";
let parsed_ipv4 = addr_parser(ipv4).unwrap();
let parsed_ipv6 = addr_parser(ipv6).unwrap();
assert_eq!(parsed_ipv4, SocketAddr::from(([10, 1, 2, 3], 8888)));
assert_eq!(
parsed_ipv6,
SocketAddr::from((Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), 8888))
);
}
#[test]
#[should_panic]
fn short_ip() {
let ip = "10.1.2:8888";
addr_parser(ip).unwrap();
}
#[test]
#[should_panic]
fn long_ip() {
let ip = "[2001:0db8:ac10:fe01:0000:0000:0000:0000:0000]:8888";
addr_parser(ip).unwrap();
}
#[test]
#[should_panic]
fn ipv6_no_brackets() {
let ip = "2001:db8::1:8888";
addr_parser(ip).unwrap();
}
#[test]
#[should_panic]
fn ip_missing_port() {
let ip = "10.1.2.3";
addr_parser(ip).unwrap();
}
}

200
src/server.rs Normal file
View File

@ -0,0 +1,200 @@
use std::{collections::HashMap, error::Error, net::SocketAddr, path::PathBuf, sync::Arc};
use log::{debug, error, info};
use tokio::{
fs::File,
io::AsyncReadExt,
net::{TcpListener, TcpStream},
sync::mpsc,
};
use crate::{crypto::Crypto, sockets::SocketHandler, util::FileInfo};
#[derive(Clone)]
pub struct Server {
addr: SocketAddr,
key: String,
chunksize: usize,
metadata: Vec<FileInfo>,
index: HashMap<String, PathBuf>,
}
impl Server {
pub fn new(
addr: SocketAddr,
key: String,
chunksize: usize,
metadata: Vec<FileInfo>,
index: HashMap<String, PathBuf>,
) -> Arc<Self> {
Arc::new(Self {
addr,
key,
chunksize,
metadata,
index,
})
}
pub async fn start(
self: Arc<Self>,
mut kill: mpsc::Receiver<()>,
bind_addr: &SocketAddr,
) -> Result<(), Box<dyn Error + Send + Sync>> {
tokio::select! {
_ = self.listen(bind_addr) => Ok(()),
_ = kill.recv() => Ok(()),
}
}
async fn listen(
self: Arc<Self>,
bind_addr: &SocketAddr,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let listener = TcpListener::bind(bind_addr).await?;
info!("Listening on {} - Access key: {}", self.addr, self.key);
loop {
let this_self = self.clone();
let (mut socket, addr) = listener.accept().await?;
info!("New client connected: {}", addr);
match tokio::spawn(async move { this_self.connection(&mut socket, &addr).await }).await
{
Ok(_) => info!("Client disconnected: {}", addr),
Err(e) => error!("Fatal error in connection {}: {}", addr, e),
};
}
}
async fn connection(
&self,
socket: &mut TcpStream,
addr: &SocketAddr,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let mut handler = SocketHandler::new(socket);
let crypto = Crypto::new(&mut handler, false).await?;
handler.set_crypto(crypto);
debug!("({}): Connection established", addr);
if !self.authorize(&mut handler, addr).await? {
info!("({}): Invalid access key", addr);
return Ok(());
}
self.metadata(&mut handler, addr).await?;
self.requests(&mut handler, addr).await?;
Ok(())
}
async fn authorize(
&self,
handler: &mut SocketHandler<'_>,
addr: &SocketAddr,
) -> Result<bool, Box<dyn Error + Send + Sync>> {
debug!("({}): Starting authorization", addr);
let buf = handler.recv().await?;
let key = String::from_utf8(buf)?;
let is_valid: bool;
let res_msg: Vec<u8>;
if key != self.key {
is_valid = false;
res_msg = b"DISCONNECT".to_vec();
} else {
is_valid = true;
res_msg = b"VALID".to_vec();
}
handler.send(&res_msg).await?;
debug!("({}): Authorization finished", addr);
Ok(is_valid)
}
async fn metadata(
&self,
handler: &mut SocketHandler<'_>,
addr: &SocketAddr,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("({}): Starting to send metadata", addr);
let amt = self.metadata.len();
let msg = amt.to_string().as_bytes().to_vec();
handler.send(&msg).await?;
let buf = handler.recv().await?;
let res_amt = String::from_utf8(buf)?.trim().parse::<usize>()?;
if res_amt != amt {
return Err("Broken message sequence during metadata exchange".into());
}
debug!("({}): Metadata amount confirmed successfully", addr);
for file in &self.metadata {
let msg = format!("{}:{}:{}", file.name, file.size, file.hash)
.as_bytes()
.to_vec();
handler.send(&msg).await?;
debug!("({}): Sent metadata of file '{}'", addr, file.hash);
}
Ok(())
}
async fn requests(
&self,
handler: &mut SocketHandler<'_>,
addr: &SocketAddr,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("({}): Waiting for file requests", addr);
loop {
let buf = handler.recv().await?;
let hash = String::from_utf8(buf)?;
let hash = hash.trim();
if hash == "DISCONNECT" {
break;
}
debug!("({}): Received request for file '{}'", addr, hash);
let mut file = File::open(self.index[hash].clone()).await?;
let mut remaining = file.metadata().await?.len();
let mut sendbuf = vec![0u8; self.chunksize];
debug!("({}): Sending bytes of '{}'", addr, hash);
while remaining != 0 {
let n = file.read(&mut sendbuf).await?;
handler.send(&sendbuf[..n]).await?;
remaining -= n as u64;
debug!("({}): {} bytes remaining", addr, remaining);
}
let buf = handler.recv().await?;
let confirmation = String::from_utf8(buf)?;
let confirmation = confirmation.trim();
if confirmation != hash {
return Err("Unsuccessful file transfer, hashes don't match".into());
}
debug!("({}): File '{}' successfully transferred", addr, hash);
}
Ok(())
}
}

103
src/sockets.rs Normal file
View File

@ -0,0 +1,103 @@
use std::error::Error;
use base64::{engine::general_purpose, Engine};
use log::debug;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
net::{
tcp::{ReadHalf, WriteHalf},
TcpStream,
},
};
use crate::crypto::Crypto;
pub struct SocketHandler<'a> {
writer: BufWriter<WriteHalf<'a>>,
reader: BufReader<ReadHalf<'a>>,
crypto: Option<Crypto>,
}
impl<'a> SocketHandler<'a> {
pub fn new(socket: &'a mut TcpStream) -> Self {
let (reader, writer) = socket.split();
let reader = BufReader::new(reader);
let writer = BufWriter::new(writer);
Self {
writer,
reader,
crypto: None,
}
}
pub fn set_crypto(&mut self, crypto: Crypto) {
// setting up AES cipher requires DH key exchange in plaintext,
// meaning crypto can't be initialized at the same time as the socket handler
debug!("Cryptography module initialized to the connection");
self.crypto = Some(crypto);
}
pub async fn send(&mut self, data: &[u8]) -> Result<(), Box<dyn Error + Send + Sync>> {
let data = match &mut self.crypto {
Some(c) => c.encrypt(data).await?,
None => data.to_vec(), // syntactic sugar, never actually called
};
let mut encoded = general_purpose::STANDARD_NO_PAD
.encode(data)
.as_bytes()
.to_vec();
encoded.push(b':');
self.send_raw(&encoded).await?;
Ok(())
}
pub async fn send_raw(&mut self, data: &[u8]) -> Result<(), Box<dyn Error + Send + Sync>> {
self.writer.write_all(data).await?;
self.writer.flush().await?;
debug!("Sent {} bytes to the socket", data.len());
Ok(())
}
pub async fn recv(&mut self) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
let mut buf = self.recv_raw(1).await?;
buf.pop();
buf = general_purpose::STANDARD_NO_PAD.decode(&buf)?.to_vec();
let data = match &self.crypto {
Some(c) => c.decrypt(&buf).await?,
None => buf,
};
Ok(data)
}
pub async fn recv_raw(
&mut self,
min_limit: usize,
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
let mut buf = Vec::new();
while buf.len() <= min_limit {
let n = self.reader.read_until(b':', &mut buf).await?;
if n == 0 {
return Err("Received 0 bytes from the socket".into());
}
}
/*
TODO: use min_limit to check whether read_until has reached EOF before reading all the necessary bytes
(e.g. regarding ecdh key exchange) --> loop and read until buf.len() == min_limit
*/
debug!("Received {} bytes from the socket", buf.len());
Ok(buf)
}
}

132
src/util.rs Normal file
View File

@ -0,0 +1,132 @@
use std::{collections::HashMap, env, error::Error, fs, net::SocketAddr, path::PathBuf};
use log::{debug, info};
use tokio::{fs::File, io::BufWriter};
use crate::crypto;
const PUBLIC_IPV4: &str = "https://ipinfo.io/ip";
const PUBLIC_IPV6: &str = "https://ipv6.icanhazip.com";
#[derive(PartialEq, Eq)]
pub enum Ip {
V4,
V6,
Local,
}
impl Ip {
pub fn fetch(self, port: u16) -> Result<(SocketAddr, SocketAddr), Box<dyn Error>> {
let addr = match self {
Ip::V4 => PUBLIC_IPV4,
Ip::V6 => PUBLIC_IPV6,
Ip::Local => {
let addr_str = format!("127.0.0.1:{}", port);
let addr = addr_str.parse::<SocketAddr>()?;
return Ok((addr, addr));
}
};
info!("Fetching IP information from {}", addr);
let res = format!("{}:{}", ureq::get(addr).call()?.into_string()?.trim(), port);
let display_addr = res.parse::<SocketAddr>()?;
let bind_addr = format!("0.0.0.0:{}", port).parse::<SocketAddr>()?;
debug!("IP: {}", res);
Ok((display_addr, bind_addr))
}
}
#[derive(Clone)]
pub struct FileInfo {
pub name: String,
pub size: u64,
pub hash: String,
}
impl FileInfo {
pub fn new(name: String, size: u64, hash: String) -> Self {
Self { name, size, hash }
}
}
pub fn filepaths(
source: Option<PathBuf>,
files: Option<Vec<PathBuf>>,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
info!("Collecting filepaths");
let mut paths = Vec::new();
if let Some(source) = source {
let home = env::var("HOME")?;
let content = fs::read_to_string(source)?;
paths = content
.lines()
.into_iter()
.map(|p| PathBuf::from(p.replace('~', &home)))
.collect();
} else if let Some(files) = files {
paths = files;
}
debug!("Filepaths collection finished (total: {})", paths.len());
Ok(paths)
}
pub async fn metadata(
files: &Vec<PathBuf>,
) -> Result<(Vec<FileInfo>, HashMap<String, PathBuf>), Box<dyn Error>> {
info!("Collecting metadata");
let mut metadata = Vec::new();
let mut index = HashMap::new();
for path in files {
debug!("Collecting '{}' metadata", path.to_str().unwrap());
let split = path.to_str().unwrap().split('/').collect::<Vec<&str>>();
let name = split[split.len() - 1].to_string();
let handle = File::open(path).await?;
let size = handle.metadata().await?.len();
let hash = crypto::try_hash(path)?;
if size > 0 {
let info = FileInfo::new(name, size, hash.clone());
metadata.push(info);
index.insert(hash, path.clone());
}
}
debug!(
"Metadata collection successfully done (total: {})",
metadata.len()
);
Ok((metadata, index))
}
pub async fn new_file(
mut path: PathBuf,
name: &str,
) -> Result<(BufWriter<File>, PathBuf), Box<dyn Error + Send + Sync>> {
debug!("New file handle for '{}'", name);
path.push(name);
let handle = File::create(&path).await?;
Ok((BufWriter::new(handle), path))
}
pub fn ascii() {
let ascii = " __
_________ ____ / /____ ____ _____
/ ___/ __ \\/ __ \\/ __/ _ \\/ __ `/ __ \\
/ /__/ /_/ / / / / /_/ __/ /_/ / /_/ /
\\___/\\____/_/ /_/\\__/\\___/\\__, /\\____/
/____/ ";
println!("{}\n", ascii);
}

0
tests/data/.placeholder Normal file
View File

View File

View File

@ -0,0 +1,101 @@
use std::{
fs::{self, File},
io::{BufWriter, Write},
path::PathBuf,
str::FromStr,
};
use contego::{
client::Client,
server::Server,
util::{metadata, Ip},
};
use env_logger::Env;
use log::debug;
use ntest::timeout;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use tokio::{fs::read_to_string, sync::mpsc};
#[tokio::test]
#[timeout(2000)]
/// Ensures backend communications integrity & the ability to handle individual requests.
async fn sockets_integration() {
env_logger::Builder::from_env(Env::default().default_filter_or("debug"))
.is_test(true)
.try_init()
.unwrap();
debug!("Initializing and starting the test");
let (testdata, paths) = testdata();
let (metadata, index) = metadata(&paths).await.unwrap();
let (display_addr, bind_addr) = Ip::Local.fetch(8080).unwrap();
let outdir = PathBuf::from("./tests/output/");
let key = String::from("testkey");
let c_key = key.clone();
let (tx, rx) = mpsc::channel::<()>(1);
let server_handle = tokio::spawn(async move {
debug!("Initializing the asynchronous server task");
let server = Server::new(display_addr, key, 8192, metadata, index);
debug!("Starting to listen to incoming connections");
server.start(rx, &bind_addr).await.unwrap();
});
let client_handle = tokio::spawn(async move {
debug!("Initializing the asynchronous client task");
let client = Client::new(display_addr, c_key, outdir);
debug!("Connecting to the server");
client.connection().await.unwrap();
});
client_handle.await.unwrap();
tx.send(()).await.unwrap();
server_handle.await.unwrap();
debug!("Checking for file integrity");
for file in testdata {
let path = String::from("./tests/output/") + file.0;
let recv_content = read_to_string(path).await.unwrap();
assert_eq!(
recv_content, file.1,
"Output '{}' doesn't match the input '{}'",
recv_content, file.1
);
fs::remove_file(String::from("./tests/output/") + file.0).unwrap();
fs::remove_file(String::from("./tests/data/") + file.0).unwrap();
debug!("File '{}' checked and removed successfully", file.0);
}
}
fn testdata() -> (Vec<(&'static str, String)>, Vec<PathBuf>) {
let mut paths = Vec::new();
let testdata = vec![
("1.txt", generate_data()),
("2.txt", generate_data()),
("3.txt", generate_data()),
];
for file in &testdata {
let filepath = PathBuf::from_str("./tests/data/").unwrap().join(file.0);
let mut writer = BufWriter::new(File::create(filepath.clone()).unwrap());
paths.push(filepath);
writer.write_all(file.1.as_bytes()).unwrap();
}
(testdata, paths)
}
fn generate_data() -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect::<String>()
}