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(()) }