Commit e44cfc63 authored by danb's avatar danb
Browse files

Use words as identifiers

parent f221ce88
Pipeline #1434 failed with stages
in 3 seconds
......@@ -4,7 +4,11 @@ version = "0.1.0"
authors = ["danb <danb@hasi.it>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "server"
[[bin]]
name = "rawr"
[dependencies]
http = { version = "0.2" }
......@@ -17,6 +21,7 @@ futures-util = { version = "0.3" }
indicatif = { version = "0.15" }
clap = { version = "3.0.0-beta.2" }
harsh = { version = "0.2" }
memorable-wordlist = { version = "0.1" }
rand = { version = "0.8" }
thiserror = { version = "1" }
......
......@@ -13,4 +13,4 @@ RUN cargo update
RUN cargo build --release
EXPOSE 80/tcp
ENTRYPOINT ["/opt/target/release/rawr-server"]
ENTRYPOINT ["/opt/target/release/server"]
[package]
name = "rawr-server"
version = "0.1.0"
authors = ["danb <danb@hasi.it>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
http = { version = "0.2" }
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.6", features = ["full"] }
futures = { version = "0.3" }
futures-util = { version = "0.3" }
harsh = { version = "0.2" }
rand = { version = "0.8" }
thiserror = { version = "1" }
......@@ -9,6 +9,8 @@ use hyper::body::{Body};
#[cfg(feature = "tls")]
use hyper_tls::HttpsConnector;
use std::path::Path;
use tokio::io;
use tokio::fs::{File, OpenOptions};
......@@ -18,33 +20,10 @@ use futures::future::join_all;
use futures_util::TryStreamExt;
use harsh::Harsh;
use rand::Rng;
use memorable_wordlist::space_delimited;
use indicatif::{ProgressBar, ProgressStyle};
use thiserror::Error;
use http::header::ToStrError;
use http::uri::InvalidUri;
use std::num::ParseIntError;
#[derive(Error, Debug)]
pub enum ClientError {
#[error("An IO error occurred")]
IOError(#[from] io::Error),
#[error("A channel send error occurred")]
ChannelSendError(#[from] tokio::sync::mpsc::error::SendError<()>),
#[error("A hyper error occurred")]
HyperError(#[from] hyper::Error),
#[error("A HTTP error occurred")]
HTTPError(#[from] http::Error),
#[error("A string conversion error occurred")]
ToStrError(#[from] ToStrError),
#[error("An integer parsing error occurred")]
ParseIntError(#[from] ParseIntError),
#[error("An uri parsing error occurred")]
UriParseError(#[from] InvalidUri),
}
use rawr::common::Error;
/// Share a file, anywhere and privacy friendly
#[derive(Clap)]
......@@ -76,8 +55,20 @@ struct Put {
/// Get a file from the web (similar to wget)
#[derive(Clap)]
struct Get {
/// URL of the file to download
url: String
/// Host that serves your file
#[clap(short, long, default_value = "share.hasi.it")]
host: String,
/// Word 1 to identify your file
word1: String,
/// Maybe word 2 to identify your file
#[clap(default_value = " ")]
word2: String,
/// Maybe word 3 to identify your file
#[clap(default_value = " ")]
word3: String,
/// Maybe word 4 to identify your file
#[clap(default_value = " ")]
word4: String,
}
fn not_found() -> Response<Body> {
......@@ -116,7 +107,7 @@ async fn response(filename: String, req: Request<Body>) -> io::Result<Response<B
}
}
async fn accept_local_downloads(file: String, count: usize) -> Result<(), ClientError> {
async fn accept_local_downloads(file: String, count: usize) -> Result<(), Error> {
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(count);
let make_service = make_service_fn(move |_| {
......@@ -146,33 +137,54 @@ async fn accept_local_downloads(file: String, count: usize) -> Result<(), Client
}
});
graceful.await.map_err(ClientError::HyperError)
graceful.await.map_err(Error::HyperError)
}
async fn put(put_cmd: Put) -> Result<(), ClientError> {
async fn put(put_cmd: Put) -> Result<(), Error> {
if !Path::new(&put_cmd.file).exists() {
println!("This file does not exist! 🐹");
return Ok(());
}
let file_clone = put_cmd.file.clone();
let accept_local_task = accept_local_downloads(file_clone, put_cmd.count);
// TODO: this does not work
let local_get_url = format!("http://{}:8080/{}", "localhost", put_cmd.file);
println!(" 🖥️ {}", local_get_url);
println!(" ➥ wget {}", local_get_url);
println!("🖥️ wget {}", local_get_url);
let server_task = async move {
let mut rng = rand::thread_rng();
let harsh = Harsh::default();
let id: u64 = rng.gen();
let hash_id = harsh.encode(&[id]);
#[cfg(feature = "tls")]
let https = HttpsConnector::new();
#[cfg(feature = "tls")]
let client = Client::builder().build::<_, hyper::Body>(https);
#[cfg(not(feature = "tls"))]
let client = Client::new();
#[cfg(feature = "tls")]
let uri = format!("https://{}/{}/{}", put_cmd.host, hash_id, put_cmd.file);
let test_uri = format!("https://{}/{}/{}", put_cmd.host, "test", "online");
#[cfg(not(feature = "tls"))]
let uri = format!("http://{}/{}/{}", put_cmd.host, hash_id, put_cmd.file);
let test_uri = format!("http://{}/{}/{}", put_cmd.host, "test", "online");
let test_res = client.get(test_uri.clone().parse()?).await?;
if test_res.status() != StatusCode::OK {
return Err(Error::NotOnlineError());
}
// TODO: how to check, if a connection is possible at all?
println!(" 🌍 {}", uri);
println!(" ➥ wget {}", uri);
let id = space_delimited(16);
// TODO: use a path module for this
let filename_split = put_cmd.file.split("/").collect::<Vec<&str>>();
let filename = filename_split.last().unwrap();
#[cfg(feature = "tls")]
let uri = format!("https://{}/{}/{}", put_cmd.host, id.replace(" ", "_"), filename);
#[cfg(not(feature = "tls"))]
let uri = format!("http://{}/{}/{}", put_cmd.host, id.replace(" ", "_"), filename);
println!("🌍 rawr get {} (or wget {})", id, uri);
let mut res_futures = Vec::new();
for _ in 0..put_cmd.count {
......@@ -181,35 +193,32 @@ async fn put(put_cmd: Put) -> Result<(), ClientError> {
let file_size = file_metadata.len();
let stream = ReaderStream::new(file);
#[cfg(feature = "tls")]
let https = HttpsConnector::new();
#[cfg(feature = "tls")]
let client = Client::builder().build::<_, hyper::Body>(https);
#[cfg(not(feature = "tls"))]
let client = Client::new();
let req_result = Request::post(uri.clone())
.header("content-length", file_size)
.body(Body::wrap_stream(stream));
match req_result {
Ok(req) => res_futures.push(client.request(req)),
Err(e) => return Err(ClientError::HTTPError(e))
Err(e) => return Err(Error::HTTPError(e))
};
}
// TODO: errors are not handled, bad
let _ = join_all(res_futures).await;
for result in join_all(res_futures).await {
result?;
}
Ok(())
};
// TODO: errors are not handled, bad
let (_, _) = tokio::join!(accept_local_task, server_task);
let (local_result, server_result) = tokio::join!(accept_local_task, server_task);
local_result?;
server_result?;
Ok(())
}
async fn get(get_cmd: Get) -> Result<(), ClientError> {
async fn get(get_cmd: Get) -> Result<(), Error> {
#[cfg(feature = "tls")]
let https = HttpsConnector::new();
#[cfg(feature = "tls")]
......@@ -218,9 +227,22 @@ async fn get(get_cmd: Get) -> Result<(), ClientError> {
#[cfg(not(feature = "tls"))]
let client = Client::new();
let uri = get_cmd.url.parse()?;
let id = format!("{} {} {} {}", get_cmd.word1, get_cmd.word2, get_cmd.word3, get_cmd.word4)
.trim()
.replace(" ", "_");
#[cfg(feature = "tls")]
let uri = format!("https://{}/{}/doesntmatter", get_cmd.host, id).parse()?;
#[cfg(not(feature = "tls"))]
let uri = format!("http://{}/{}/doesntmatter", get_cmd.host, id).parse()?;
let res = client.get(uri).await?;
if res.status() == StatusCode::NOT_FOUND {
println!("File not found!");
return Ok(());
}
// TODO: unchecked element access
let filename = res.headers()
.get("content-disposition")
......@@ -257,7 +279,7 @@ async fn get(get_cmd: Get) -> Result<(), ClientError> {
}
#[tokio::main]
async fn main() -> Result<(), ClientError> {
async fn main() -> Result<(), Error> {
let opts: Opts = Opts::parse();
match opts.sub_cmd {
SubCommand::Put(put_cmd) => {
......
......@@ -15,42 +15,16 @@ use tokio::io;
use futures::future::TryFutureExt;
use futures_util::StreamExt;
use harsh::Harsh;
use thiserror::Error;
use http::header::ToStrError;
use http::uri::InvalidUri;
use std::num::ParseIntError;
use std::net::AddrParseError;
#[derive(Error, Debug)]
pub enum ServerError {
#[error("An IO error occurred")]
IOError(#[from] io::Error),
#[error("A channel send error occurred")]
ChannelSendError(#[from] tokio::sync::mpsc::error::SendError<()>),
#[error("A hyper error occurred")]
HyperError(#[from] hyper::Error),
#[error("A string conversion error occurred")]
ToStrError(#[from] ToStrError),
#[error("An integer parsing error occurred")]
ParseIntError(#[from] ParseIntError),
#[error("An uri parsing error occurred")]
UriParseError(#[from] InvalidUri),
#[error("An address parsing error occurred")]
AddrParseError(#[from] AddrParseError),
}
use rawr::common::Error;
struct FileStreamMap(HashMap<u64, Vec<(Body, u64, Sender<()>)>>);
struct FileStreamMap(HashMap<String, Vec<(Body, String, u64, Sender<()>)>>);
impl FileStreamMap {
pub fn new() -> FileStreamMap {
FileStreamMap(HashMap::new())
}
pub fn add(&mut self, id: u64, file_stream: (Body, u64, Sender<()>)) -> () {
pub fn add(&mut self, id: String, file_stream: (Body, String, u64, Sender<()>)) -> () {
let id_present = self.0.contains_key(&id);
if !id_present {
self.0.insert(id, vec![file_stream]);
......@@ -60,7 +34,7 @@ impl FileStreamMap {
}
}
pub fn consume(&mut self, id: u64) -> Option<(Body, u64, Sender<()>)> {
pub fn consume(&mut self, id: String) -> Option<(Body, String, u64, Sender<()>)> {
let option = match self.0.get_mut(&id) {
Some(vec) => Some(vec.remove(0)),
None => None
......@@ -83,52 +57,52 @@ fn not_found() -> Response<Body> {
.unwrap()
}
async fn response(files_table: Arc<Mutex<FileStreamMap>>,
req: Request<Body>) -> Result<Response<Body>, ServerError> {
let harsh = Harsh::default();
fn test_ok() -> Response<Body> {
Response::builder()
.status(StatusCode::OK)
.body("ok".into())
.unwrap()
}
async fn response(files_table: Arc<Mutex<FileStreamMap>>,
req: Request<Body>) -> Result<Response<Body>, Error> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/test/online") => {
Ok(test_ok())
}
(&Method::GET, path) => {
let chunks: Vec<&str> = path.split("/").collect();
if chunks.len() < 3 {
if chunks.len() < 2 {
return Ok(not_found());
}
// We ignore the '/' at the beginning of the path
if let Ok(decoded) = harsh.decode(chunks[1]) {
let id = decoded[0];
let filename = chunks[2];
match files_table.lock().await.consume(id) {
Some((req_body, len, res_tx)) => {
let res_body = req_body.map(move |chunk_result| {
let _ = res_tx.send(());
chunk_result
});
Response::builder()
.header("content-length", len)
.header("content-type", "application/octet-stream")
.header("content-disposition",
format!("attachment; filename=\"{}\"", filename))
//.body(res_body)
.body(Body::wrap_stream(res_body))
.or_else(|_| Ok(not_found()))
},
None => {
// The id is not in the hash map
Ok(not_found())
}
let id = chunks[1].to_string();
match files_table.lock().await.consume(id) {
Some((req_body, filename, len, res_tx)) => {
let res_body = req_body.map(move |chunk_result| {
let _ = res_tx.send(());
chunk_result
});
Response::builder()
.header("content-length", len)
.header("content-type", "application/octet-stream")
.header("content-disposition",
format!("attachment; filename=\"{}\"", filename))
//.body(res_body)
.body(Body::wrap_stream(res_body))
.or_else(|_| Ok(not_found()))
},
None => {
// The id is not in the hash map
Ok(not_found())
}
}
else
{
// The hash id could not be decoded
Ok(not_found())
}
},
(&Method::POST, path) => {
let chunks: Vec<&str> = path.split("/").collect();
if chunks.len() < 2 {
if chunks.len() < 3 {
return Ok(not_found());
}
......@@ -137,17 +111,16 @@ async fn response(files_table: Arc<Mutex<FileStreamMap>>,
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
// We ignore the '/' at the beginning of the path
if let Ok(decoded) = harsh.decode(chunks[1]) {
let id = decoded[0];
let len = req.headers()
.get("content-length")
.ok_or(io::Error::new(io::ErrorKind::Other, "No Content-Length header field!"))?
.to_str()?
.parse()?;
let body = req.into_body();
files_table.lock().await.add(id, (body, len, tx));
}
let id = chunks[1].to_string();
let filename = chunks[2].to_string();
let len = req.headers()
.get("content-length")
.ok_or(io::Error::new(io::ErrorKind::Other, "No Content-Length header field!"))?
.to_str()?
.parse()?;
let body = req.into_body();
files_table.lock().await.add(id, (body, filename, len, tx));
// This is a hack (as long as the channel is not dropped, we get Some(()))
while let Some(()) = rx.recv().await {}
......@@ -161,7 +134,7 @@ async fn response(files_table: Arc<Mutex<FileStreamMap>>,
}
}
async fn accept_downloads(files_table: Arc<Mutex<FileStreamMap>>) -> Result<(), ServerError> {
async fn accept_downloads(files_table: Arc<Mutex<FileStreamMap>>) -> Result<(), Error> {
println!("Initializing HTTP server.");
let make_service = make_service_fn(move |_| {
......@@ -177,10 +150,10 @@ async fn accept_downloads(files_table: Arc<Mutex<FileStreamMap>>) -> Result<(),
let addr = "0.0.0.0:80".parse()?;
let server = Server::bind(&addr).serve(make_service);
server.await.map_err(ServerError::HyperError)
server.await.map_err(Error::HyperError)
}
#[tokio::main]
async fn main() -> Result<(), ServerError> {
async fn main() -> Result<(), Error> {
accept_downloads(Arc::new(Mutex::new(FileStreamMap::new()))).await
}
use thiserror::Error;
use tokio::io;
use http::header::ToStrError;
use http::uri::InvalidUri;
use std::num::ParseIntError;
use std::net::AddrParseError;
#[derive(Error, Debug)]
pub enum Error {
#[error("An IO error occurred")]
IOError(#[from] io::Error),
#[error("A channel send error occurred")]
ChannelSendError(#[from] tokio::sync::mpsc::error::SendError<()>),
#[error("A hyper error occurred")]
HyperError(#[from] hyper::Error),
#[error("A HTTP error occurred")]
HTTPError(#[from] http::Error),
#[error("A string conversion error occurred")]
ToStrError(#[from] ToStrError),
#[error("An integer parsing error occurred")]
ParseIntError(#[from] ParseIntError),
#[error("An uri parsing error occurred")]
UriParseError(#[from] InvalidUri),
#[error("An address parsing error occurred")]
AddrParseError(#[from] AddrParseError),
#[error("Either you or the server are not online")]
NotOnlineError(),
}
pub mod common;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment