main.rs 13.9 KB
Newer Older
1
2
#![cfg_attr(feature = "strict", deny(warnings))]

Robert Czechowski's avatar
Robert Czechowski committed
3
4
5
6
7
8
9
#[macro_use]
extern crate iron;
#[macro_use]
extern crate router;
#[macro_use]
extern crate serde_derive;

Robert Czechowski's avatar
Robert Czechowski committed
10
extern crate handlebars_iron;
Robert Czechowski's avatar
Robert Czechowski committed
11
12
extern crate iron_sessionstorage;
extern crate mount;
13
extern crate params;
Robert Czechowski's avatar
Robert Czechowski committed
14
extern crate persistent;
15
#[cfg(feature = "postgres")]
16
extern crate postgres;
Robert Czechowski's avatar
Robert Czechowski committed
17
extern crate rand;
18
extern crate reqwest;
19
#[cfg(feature = "rusqlite")]
Robert Czechowski's avatar
Robert Czechowski committed
20
21
extern crate rusqlite;
extern crate serde_json;
22
extern crate serde_yaml;
Robert Czechowski's avatar
Robert Czechowski committed
23
24
25
26
extern crate staticfile;
extern crate structopt;
extern crate time;
extern crate urlencoded;
27
#[cfg(feature = "webbrowser")]
28
extern crate webbrowser;
29
30

mod db_apply_migrations;
31
pub mod db_conn;
32
mod db_conn_postgres;
33
mod db_conn_sqlite;
34
mod db_objects;
35
pub mod config;
36
pub mod contestreader_yaml;
37
38
39
40
pub mod functions;
pub mod oauth_provider;
mod webfw_iron;

41
use db_conn::{MedalConnection, MedalObject};
Robert Czechowski's avatar
Robert Czechowski committed
42
use functions::SetPassword; // TODO: Refactor, so we don't need to take this from there!
43
44
45

use db_objects::*;

Robert Czechowski's avatar
Robert Czechowski committed
46
47
48
use webfw_iron::start_server;

use std::fs;
Robert Czechowski's avatar
Robert Czechowski committed
49
use std::path;
Robert Czechowski's avatar
Robert Czechowski committed
50

51
use std::path::Path;
52
53
use structopt::StructOpt;

54
use config::Config;
55

Robert Czechowski's avatar
Robert Czechowski committed
56
fn read_contest(p: &path::PathBuf) -> Option<Contest> {
57
58
    use std::fs::File;
    use std::io::Read;
59

60
61
62
    let mut file = File::open(p).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
63

64
    contestreader_yaml::parse_yaml(&contents,
65
66
                                   p.file_name().to_owned()?.to_str()?,
                                   &format!("{}/", p.parent().unwrap().to_str()?))
Robert Czechowski's avatar
Robert Czechowski committed
67
68
69
70
}

fn get_all_contest_info(task_dir: &str) -> Vec<Contest> {
    fn walk_me_recursively(p: &path::PathBuf, contests: &mut Vec<Contest>) {
71
72
        if let Ok(paths) = fs::read_dir(p) {
            for path in paths {
Robert Czechowski's avatar
Robert Czechowski committed
73
74
                let p = path.unwrap().path();
                walk_me_recursively(&p, contests);
75
            }
Robert Czechowski's avatar
Robert Czechowski committed
76
        }
77

78
        if p.file_name().unwrap().to_string_lossy().to_string().ends_with(".yaml") {
79
            read_contest(p).map(|contest| contests.push(contest));
80
        };
Robert Czechowski's avatar
Robert Czechowski committed
81
82
83
84
85
    };

    let mut contests = Vec::new();
    match fs::read_dir(task_dir) {
        Err(why) => println!("Error opening tasks directory! {:?}", why.kind()),
Robert Czechowski's avatar
Robert Czechowski committed
86
87
88
89
90
        Ok(paths) => {
            for path in paths {
                walk_me_recursively(&path.unwrap().path(), &mut contests);
            }
        }
Robert Czechowski's avatar
Robert Czechowski committed
91
92
93
94
95
    };

    contests
}

96
97
98
99
fn refresh_all_contests<C>(conn: &mut C)
    where C: MedalConnection,
          db_objects::Contest: db_conn::MedalObject<C>
{
Robert Czechowski's avatar
Robert Czechowski committed
100
101
102
103
104
105
106
    let v = get_all_contest_info("tasks/");

    for mut contest_info in v {
        contest_info.save(conn);
    }
}

107
108
fn add_admin_user<C>(conn: &mut C, resetpw: bool)
    where C: MedalConnection {
109
110
111
    let mut admin = match conn.get_user_by_id(1) {
        None => {
            print!("New Database. Creating new admin user with credentials 'admin':");
112
            conn.new_session("")
Robert Czechowski's avatar
Robert Czechowski committed
113
        }
114
115
        Some(user) => {
            if !resetpw {
Robert Czechowski's avatar
Robert Czechowski committed
116
                return;
117
            }
118
119
120
121
122
            print!("Request to reset admin password. Set credentials 'admin':");
            user
        }
    };

Robert Czechowski's avatar
Robert Czechowski committed
123
    use rand::{distributions::Alphanumeric, thread_rng, Rng};
124
125

    let password: String = thread_rng().sample_iter(&Alphanumeric)
Robert Czechowski's avatar
Robert Czechowski committed
126
127
128
129
130
131
                                       .filter(|x| {
                                           let x = *x;
                                           !(x == 'l' || x == 'I' || x == '1' || x == 'O' || x == 'o' || x == '0')
                                       })
                                       .take(8)
                                       .collect();
132
133
134
135
    print!("'{}' …", &password);

    admin.username = Some("admin".into());
    match admin.set_password(&password) {
136
        None => println!(" FAILED! (Password hashing error)"),
137
138
        _ => {
            conn.save_session(admin);
139
            println!(" Done");
140
        }
141
142
143
    }
}

144
145
146
147
148
149
150
fn prepare_and_start_server<C>(mut conn: C, config: Config, onlycontestscan: bool, resetadminpw: bool)
    where C: MedalConnection + std::marker::Send + 'static,
          db_objects::Contest: db_conn::MedalObject<C>
{
    db_apply_migrations::test(&mut conn);

    if onlycontestscan || config.no_contest_scan == Some(false) {
151
        print!("Scanning for contests …");
152
        refresh_all_contests(&mut conn);
153
        println!(" Done")
154
155
156
157
158
    }

    if !onlycontestscan {
        add_admin_user(&mut conn, resetadminpw);

159
        #[cfg(feature = "webbrowser")]
160
        let self_url = config.self_url.clone();
161
        #[cfg(feature = "webbrowser")]
162
163
        let open_browser = config.open_browser;

164
        match start_server(conn, config) {
165
166
            Ok(_) => {
                println!("Server started");
167
168
169
170
171
                #[cfg(feature = "webbrowser")]
                {
                    if let (Some(self_url), Some(true)) = (self_url, open_browser) {
                        open_browser_window(&self_url);
                    }
172
                }
173
            }
174
175
            Err(_) => println!("Error on server start …"),
        };
176

177
178
179
180
        println!("Could not run server. Is the port already in use?");
    }
}

181
#[cfg(feature = "webbrowser")]
182
183
184
fn open_browser_window(self_url: &str) {
    match webbrowser::open(&self_url) {
        Ok(_) => (),
185
        Err(e) => println!("Error while opening webbrowser: {:?}", e),
186
187
188
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
189
fn main() {
190
    let opt = config::Opt::from_args();
191
    //println!("{:?}", opt); // Show in different debug level?
Daniel Brüning's avatar
Daniel Brüning committed
192

193
    let mut config = config::read_config_from_file(&opt.configfile);
194

Robert Czechowski's avatar
Robert Czechowski committed
195
196
197
    if opt.databasefile.is_some() {
        config.database_file = opt.databasefile;
    }
198
199
200
    if config.database_file.is_none() {
        config.database_file = Some(Path::new("medal.db").to_owned())
    }
201
202
203
    if opt.databaseurl.is_some() {
        config.database_url = opt.databaseurl;
    }
Robert Czechowski's avatar
Robert Czechowski committed
204
205
206
    if opt.port.is_some() {
        config.port = opt.port;
    }
207
208
209
    if opt.nocontestscan {
        config.no_contest_scan = Some(true);
    }
210
211
212
    if opt.openbrowser {
        config.open_browser = Some(true)
    }
213

214
215
216
217
218
219
220
221
222
223
    #[cfg(feature = "postgres")]
    {
        if let Some(url) = config.database_url.clone() {
            print!("Using database {} … ", &url);
            let conn = postgres::Connection::connect(url, postgres::TlsMode::None).unwrap();
            println!("Connected");

            prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw);
            return;
        }
224
    }
225
226
227
228
229
230
231
232
233
234
235
236
237
238

    #[cfg(feature = "rusqlite")]
    {
        if let Some(path) = config.database_file.clone() {
            print!("Using database file {} … ", &path.to_str().unwrap_or("<unprintable filename>"));
            let conn = rusqlite::Connection::open(path).unwrap();
            println!("Connected");

            prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw);
            return;
        }
    }

    println!("No database configured. Try enableing the 'rusqlite' feature during compilation.\nLeaving now.");
239
}
240
241
242
243

#[cfg(test)]
mod tests {
    use super::*;
244
    use std::io::Read;
245

Robert Czechowski's avatar
Robert Czechowski committed
246
247
    fn start_server_and_fn<F>(port: u16, set_user: Option<(String, String)>, f: F)
        where F: FnOnce() {
248
        use std::sync::mpsc::channel;
Robert Czechowski's avatar
Robert Czechowski committed
249
        use std::{thread, time};
250
251
252
253
        let (start_tx, start_rx) = channel();
        let (stop_tx, stop_rx) = channel();

        thread::spawn(move || {
254
            let mut conn = rusqlite::Connection::open_in_memory().unwrap();
255
256
            db_apply_migrations::test(&mut conn);

257
            if let Some(user) = set_user {
258
                let mut test_user = conn.new_session("");
259
                test_user.username = Some(user.0);
260
261
262
                match test_user.set_password(&user.1) {
                    None => panic!("Set Password did not work correctly.)"),
                    _ => conn.save_session(test_user),
263
264
265
                }
            }

266
            let mut config = config::read_config_from_file(Path::new("thisfileshoudnotexist"));
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
            config.port = Some(port);
            let srvr = start_server(conn, config);

            start_tx.send(()).unwrap();

            stop_rx.recv().unwrap();

            srvr.unwrap().close().unwrap();
        });

        // wait for server to start:
        start_rx.recv().unwrap();
        thread::sleep(time::Duration::from_millis(100));
        f();
        stop_tx.send(()).unwrap();
    }

284
    fn login_for_tests(port: u16, client: &reqwest::Client, username: &str, password: &str) -> reqwest::Response {
285
        let params = [("username", username), ("password", password)];
Robert Czechowski's avatar
Robert Czechowski committed
286
        let resp = client.post(&format!("http://localhost:{}/login", port)).form(&params).send().unwrap();
287
        resp
288
289
    }

290
291
292
293
294
295
296
    fn check_status(resp: &reqwest::Response, expected_status: reqwest::StatusCode) {
        let status = resp.status();
        if status != &expected_status {
            panic!("Status is not (as expexted) {}. Status: {}", expected_status, status)
        };
    }

297
298
    #[test]
    fn start_server_and_check_request() {
Robert Czechowski's avatar
Robert Czechowski committed
299
        start_server_and_fn(8080, None, || {
300
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
301
            check_status(&resp, reqwest::StatusCode::Ok);
302
            let mut content = String::new();
303
            resp.read_to_string(&mut content).unwrap();
304
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
305
306
307
            assert!(!content.contains("Error"));

            let mut resp = reqwest::get("http://localhost:8080/contest").unwrap();
308
            check_status(&resp, reqwest::StatusCode::Ok);
309
            let mut content = String::new();
310
            resp.read_to_string(&mut content).unwrap();
311
312
313
314
            assert!(content.contains("<h1>Wettbewerbe</h1>"));
            assert!(!content.contains("Error"));
        })
    }
Daniel Brüning's avatar
Daniel Brüning committed
315

316
317
    #[test]
    fn check_login_wrong_credentials() {
Robert Czechowski's avatar
Robert Czechowski committed
318
        start_server_and_fn(8081, None, || {
319
            let client = reqwest::Client::new().unwrap();
320
321
            let mut resp = login_for_tests(8081, &client, "nonexistingusername", "wrongpassword");
            check_status(&resp, reqwest::StatusCode::Ok);
322
            let mut content = String::new();
323
            resp.read_to_string(&mut content).unwrap();
324
325
            assert!(content.contains("<h1>Login</h1>"));
            assert!(content.contains("Login fehlgeschlagen."));
326
            assert!(!content.contains("Error"));
327
        })
328
    }
329
330
331

    #[test]
    fn start_server_and_check_login() {
Robert Czechowski's avatar
Robert Czechowski committed
332
        start_server_and_fn(8082, Some(("testusr".to_string(), "testpw".to_string())), || {
333
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
334
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
335
336
337
            let mut resp = login_for_tests(8082, &client, "testusr", "testpw");
            check_status(&resp, reqwest::StatusCode::Found);

338
            let mut content = String::new();
339
            resp.read_to_string(&mut content).unwrap();
340
341
342
            assert!(!content.contains("Error"));

            let header = resp.headers();
343
344
            let set_cookie = header.get::<reqwest::header::SetCookie>();
            match set_cookie {
345
                None => panic!("No setCookie."),
Robert Czechowski's avatar
Robert Czechowski committed
346
347
                Some(cookie) => {
                    if cookie.len() == 1 {
348
                        let new_cookie = reqwest::header::Cookie(cookie.to_vec());
Robert Czechowski's avatar
Robert Czechowski committed
349
                        let mut new_resp = client.get("http://localhost:8082").header(new_cookie).send().unwrap();
350
351
352
353
354
355
                        check_status(&new_resp, reqwest::StatusCode::Ok);

                        let mut new_content = String::new();
                        new_resp.read_to_string(&mut new_content).unwrap();
                        assert!(!content.contains("Error"));
                        assert!(new_content.contains("Eingeloggt als <em>testusr</em>"));
356
                        assert!(new_content.contains("Jugendwettbewerb Informatik</h1>"));
357
358
                    } else {
                        panic!("More than one setCookie.");
Robert Czechowski's avatar
Robert Czechowski committed
359
360
                    }
                }
361
362
363
364
            };
        })
    }

365
366
    #[test]
    fn start_server_and_check_logout() {
Robert Czechowski's avatar
Robert Czechowski committed
367
        start_server_and_fn(8083, Some(("testusr".to_string(), "testpw".to_string())), || {
368
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
369
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
370
            let resp = login_for_tests(8083, &client, "testusr", "testpw");
371
            check_status(&resp, reqwest::StatusCode::Found);
372
373

            let header = resp.headers();
374
375
            let set_cookie = header.get::<reqwest::header::SetCookie>();
            match set_cookie {
376
                None => panic!("No setCookie."),
Robert Czechowski's avatar
Robert Czechowski committed
377
378
379
380
381
382
383
384
385
386
387
                Some(cookie) => {
                    if cookie.len() == 1 {
                        let new_cookie = reqwest::header::Cookie(cookie.to_vec());
                        let mut new_resp =
                            client.get("http://localhost:8082/logout").header(new_cookie.clone()).send().unwrap();
                        check_status(&new_resp, reqwest::StatusCode::Found);
                        new_resp = client.get("http://localhost:8082").header(new_cookie).send().unwrap();
                        check_status(&new_resp, reqwest::StatusCode::Ok);

                        let mut new_content = String::new();
                        new_resp.read_to_string(&mut new_content).unwrap();
388
389
390
391
                        assert!(new_content.contains("Benutzername"));
                        assert!(new_content.contains("Passwort"));
                        assert!(new_content.contains("Gruppencode / Teilnahmecode"));
                        assert!(new_content.contains("Jugendwettbewerb Informatik</h1>"));
392
393
                    } else {
                        panic!("More than one setCookie.");
Robert Czechowski's avatar
Robert Czechowski committed
394
395
                    }
                }
396
397
398
399
            };
        })
    }

400
401
402
403
404
405
406
407
408
    #[test]
    fn check_group_creation_and_group_code_login() {
        start_server_and_fn(8084, Some(("testusr".to_string(), "testpw".to_string())), || {
            let mut client = reqwest::Client::new().unwrap();
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
            let resp = login_for_tests(8084, &client, "testusr", "testpw");
        })
    }

409
}