summaryrefslogtreecommitdiff
path: root/server/src/main.rs
blob: b718d570b622cfa2fb8663c2119433a96ee482b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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<Body>, remote_addr: &SocketAddr, settings: &Settings) -> Option<Response<Body>> {
    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<Body>, remote_addr: &SocketAddr, settings: &Settings) -> Result<Response<Body>, 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<dyn std::error::Error + Send + Sync>> {

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