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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! A dropbox to collect submissions.
//! 
//! This is a webserver that accepts Submissions in JSON format
//! and writes them to a CSV file.
//! 
//! Note that this has no relation to [Dropbox](https://www.dropbox.com/), and doesn't 
//! function the same at all.
//! 
//! You should run this on a publicly available server and be sure 
//! the correct ports are open. You can run this on whatever port you'd like,
//! as long as you have permission. The `/submit` route is meant to accept a Submission.
//! See the [`helpers::web::post_json`](helpers::web::post_json) function for more info on
//! how to send a submission to the dropbox.

// Re exports to be available from this module
pub mod results_file;
pub mod submission;
pub mod fingerprint;

pub use results_file::{AsCsv, ResultsFile};
pub use submission::{Submission, TestData};


// std uses
use std::env;
use std::sync::Mutex;

// external uses
use rocket::{Rocket, Config, State};
use rocket::http::Status;
use rocket::error::LaunchError;
use rocket::config::Environment;
use rocket_contrib::json::Json;


/// A ResultsFile wrapped in a Mutex for thread locking.
/// This allows concurrent acceptance of submissions without
/// screwing up the results file.
struct SharedResultsFile(Mutex<ResultsFile>);

/// Just a test route so you can make sure the server is running
#[get("/")]
fn return_ok() -> Status {
    Status::Ok
}

/// Accepts a submission and writes it to the results file
#[post("/submit", format = "application/json", data = "<submission>")]
fn accept_submission(state: State<SharedResultsFile>, submission: Json<Submission>) -> Status {
    // Retrieve the submission
    let sub = submission.into_inner();

    // Lock the results file until we're done with it
    let shared_rf: &SharedResultsFile = state.inner();
    let mut lock = shared_rf.0.lock().expect("Lock shared results file");

    // Write the header based on first submission
    if lock.length() == 0 {
        if lock.append(&sub.header()).is_err() {
            eprintln!("Error! Could not write csv file header. File is likely locked by another process");
            return Status::InternalServerError;
        };
    }

    if lock.write_csv(&sub).is_ok() {
        return Status::Accepted;
    } else {
        eprintln!("Error: Could not write following submission");
        eprintln!("{:#?}", sub);
        return Status::InternalServerError;
    }
}

/// Builds a rocket instance to launch
fn new_rocket(port: u16) -> Rocket {
    // If debug
    #[cfg(debug_assertions)]
    let builder = Config::build(Environment::Development);
    // If production
    #[cfg(not(debug_assertions))]
    let builder = Config::build(Environment::Production);

    let config = builder
        .address("0.0.0.0")
        .port(port)
        .finalize()
        .expect("Could not build dropbox server");

    // Create a results file wrapped in a Mutex
    // The muted is necessary because route handling is asyncronous.
    // This allows mutliple submissions to be submitted at once. It also will
    // crash when the instructor opens the dropbox if the file is already in use.
    let shared_results_file = SharedResultsFile(
        Mutex::new(
            ResultsFile::new_blank("submissions.csv").expect("Couldn't open results file")
        )
    );

    println!("Dropbox is open! accepting POST requests to /submit");
    return rocket::custom(config)
        .manage(shared_results_file)
        .mount("/", routes![return_ok, accept_submission]);
}

/// Opens the dropbox for submissions on the given port.
/// 
/// You should probably use [`open_with_arg()`](crate::dropbox::open_with_arg).
pub fn open(port: u16) -> LaunchError {
    new_rocket(port).launch()
}

/// This is the same as [`open()`](crate::dropbox::open), but it will
/// only open the dropbox if you run the executable with the arg you provide.
/// 
/// The dropbox will open if the provided arg is *anywhere* in the arg vector, ie.
/// position doesn't matter. 
/// 
/// It's probably a good idea to put something that isn't obvious as the arg, perhaps
/// a password. That way, no one will accidentally (or malicously) opens the dropbox.
/// 
/// ```no_compile
/// // Must run the execuable with `my_grader open_sesame`
/// dropbox::open_with_arg(8080, "open_sesame");
/// ```
pub fn open_with_arg(arg: &str, port: u16) -> Option<LaunchError> {
    let args: Vec<String> = env::args().collect();
    if args.contains(&String::from(arg)) {
        return Some(open(port));
    }
    None
}


#[cfg(test)]
mod tests {
    use super::*;
    use rocket::local::Client;
    use rocket::http::Header;

    fn client() -> Client {
        let rocket = new_rocket(8080);
        Client::new(rocket).expect("valid rocket instance")
    }

    #[test]
    fn test_server_runs() {
        let client = client();
        let resp = client.get("/").dispatch();

        assert_eq!(resp.status(), Status::Ok);
    }

    #[test]
    fn test_404() {
        let client = client();
        let resp = client.get("/not_a_route").dispatch();

        assert_eq!(resp.status(), Status::NotFound);
    }

    #[test]
    fn test_422_unprocessable_entity() {
        let client = client();
        let resp = client.post("/submit")
            // This route only accepts Submissions
            // This is not a submission
            .body(r#"{"key":"value"}"#)
            // This should be set automatically by body but it doesn't work lmao
            .header(Header::new("Content-Type", "application/json"))
            .dispatch();

        assert_eq!(resp.status(), Status::UnprocessableEntity)
    }

    #[test]
    fn test_accept_submission() {
        let client = client();
        let sub = Submission::new();
        let req = client.post("/submit")
            .body(serde_json::to_string(&sub).unwrap())
            .header(Header::new("Content-Type", "application/json"))
            .dispatch();

        assert_eq!(req.status(), Status::Accepted);
    }
}