From e0c6f2cd7a2ef02fb9b7947816b54f3a43ef522d Mon Sep 17 00:00:00 2001 From: stuebinm Date: Tue, 27 Apr 2021 17:40:40 +0200 Subject: server: add config options, better outfile names e.g. - ability to log ip addresses of respondents - configurable bind address - reasonably sane defaults (hopefully) - survey name as part of output file names --- server/Cargo.lock | 90 +++++++++++++++++++++++++++++++++++++++ server/Cargo.toml | 1 + server/src/main.rs | 121 +++++++++++++++++++++++++++++++++++++++++------------ todo.org | 5 ++- 4 files changed, 188 insertions(+), 29 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 5800be3..a7a7799 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -33,6 +33,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "bitflags", + "textwrap", + "unicode-width", +] + [[package]] name = "cpuid-bool" version = "0.1.2" @@ -181,6 +192,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.18" @@ -425,6 +445,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -478,6 +522,7 @@ dependencies = [ "hex", "hyper", "sha2", + "structopt", "tokio", ] @@ -525,6 +570,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "structopt" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.70" @@ -536,6 +605,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "tokio" version = "1.5.0" @@ -619,6 +697,18 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index 82b4f20..8fbbea2 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -12,3 +12,4 @@ tokio = { version = "1", features = ["full"] } futures = "0.3" sha2 = "0.9.3" hex = "0.4.3" +structopt = { version = "0.3", default-features = false } diff --git a/server/src/main.rs b/server/src/main.rs index ec2c77c..b718d57 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,36 +1,92 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; +use hyper::server::conn::AddrStream; use sha2::{Sha256, Digest}; use std::io::Write; +use std::convert::Infallible; +use std::net::SocketAddr; +use std::path::PathBuf; +use structopt::StructOpt; + + + +#[derive(Debug, StructOpt, Clone)] +#[structopt(about = "simple server that eats slightly better surveys.")] +struct Settings { + #[structopt(short, long, help="log ip addresses of answers as part of their filename")] + log_ips: bool, + #[structopt(default_value="127.0.0.1:8000", help="address which the server should listen on")] + bind_address: SocketAddr, + #[structopt(parse(from_os_str), default_value=".", help="where to save incoming survey answers")] + out_path: PathBuf +} + + +async fn handle_survey(req: Request, remote_addr: &SocketAddr, settings: &Settings) -> Option> { + let (parts, rawbody) = req.into_parts(); + let body = hyper::body::to_bytes(rawbody).await.ok()?; + let survey = parts.headers.get("survey")?; + + let hash = { + // we can use a hash of the content as filename; + // age ensures we won't have collisions even if + // two people handed in the same thing twice. + let mut hasher = Sha256::new(); + hasher.update(body.clone()); + hasher.finalize() + }; + + let mut filepath = settings.out_path.clone(); + filepath.set_file_name(match settings.log_ips { + true => { + let filename = format!( + "answers-{}-{}-{:.20}.age", + survey.to_str().ok()?, + remote_addr.ip(), + hex::encode(hash) + ); + println!( + "received survey response for survey {:?} from {}, saving to {}", + survey, + remote_addr.ip(), + filename + ); + filename + }, + false => { + let filename = format!( + "answers-{}-{:.20}.age", + survey.to_str().ok()?, + hex::encode(hash) + ); + println!( + "received survey response for survey {:?}, saving to {}", + survey, + filename + ); + filename + } + }); + + let mut file = std::fs::File::create(filepath).ok()?; + file.write_all(&body).unwrap(); + + Some(Response::new( + "thanks for handing in these answers!\n".into() + )) +} /// This is our service handler. It receives a Request, routes on its /// path, and returns a Future of a Response. -async fn echo(req: Request) -> Result, hyper::Error> { +async fn server(req: Request, remote_addr: &SocketAddr, settings: &Settings) -> Result, Infallible> { match (req.method(), req.uri().path()) { - // Serve some instructions at / - (&Method::GET, "/") => Ok(Response::new(Body::from( - "Try POSTing data to /echo such as: `curl localhost:8000/echo -XPOST -d 'hello world'`", - ))), - - // Simply echo the body back to the client. - (&Method::POST, "/echo") => Ok(Response::new(req.into_body())), (&Method::POST, "/survey") => { - let full_body = hyper::body::to_bytes(req.into_body()).await?; - let mut hasher = Sha256::new(); - hasher.update(full_body.clone()); - let hash = hasher.finalize(); - - let mut file = std::fs::File::create( - format!("answers-{}.age", hex::encode(hash)) - ).unwrap(); - - file.write_all(&full_body).unwrap(); - println!("got hash: {:?}", hash); - Ok(Response::new( - "thanks for hading in these answers!".into() - )) + match handle_survey(req, remote_addr, settings).await { + Some(response) => Ok(response), + None => Ok(Response::new("Something went wrong\n".into())) + } } // Return the 404 Not Found for other routes. @@ -46,17 +102,28 @@ async fn echo(req: Request) -> Result, hyper::Error> { #[tokio::main] async fn main() -> Result<(), Box> { - let addr = ([127, 0, 0, 1], 8000).into(); + // this is a memory leak of constant size, to avoid the borrow checker + let opt: &'static Settings = Box::leak( + Box::new(Settings::from_args()) + ); + + // I have no idea what's wrong here ... + // look at https://docs.rs/hyper/0.14.7/hyper/service/fn.make_service_fn.html let service = make_service_fn( - |_| async { - Ok::<_, hyper::Error>(service_fn(echo)) + |socket: &AddrStream| { + let remote_addr = socket.remote_addr(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| async move { + server(req, &remote_addr, &opt).await + })) + } } ); - let server = Server::bind(&addr).serve(service); + let server = Server::bind(&opt.bind_address).serve(service); - println!("Listening on http://{}", addr); + println!("Listening on http://{}", opt.bind_address); server.await?; diff --git a/todo.org b/todo.org index 746f97c..9e3154f 100644 --- a/todo.org +++ b/todo.org @@ -13,12 +13,13 @@ * TODO Server ** DONE possibly switch from guile to rust or something ** TODO implement missing features: -*** TODO truncate hashes +*** DONE truncate hashes *** TODO accept unencrypted answers and store them in a single file? *** TODO validate answers? This seems difficult, but I guess it could at least check whether the age-encryption header is there. -*** TODO config options: listen address, output path +*** DONE config options: listen address, output path +*** TODO limit answers from one ip address ** TODO short description of what it does ** TODO nix module -- cgit v1.2.3