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 server(req: Request, remote_addr: &SocketAddr, settings: &Settings) -> Result, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::POST, "/survey") => {
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.
_ => {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body("nothing here!\n".into())
.unwrap()
)
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box> {
// 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(
|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(&opt.bind_address).serve(service);
println!("Listening on http://{}", opt.bind_address);
server.await?;
Ok(())
}