main.rs 16.4 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
31
pub mod config;
pub mod contestreader_yaml;
32
pub mod db_conn;
33
34
35
36
pub mod functions;
pub mod oauth_provider;

mod db_apply_migrations;
37
mod db_conn_postgres;
38
mod db_conn_sqlite;
39
mod db_objects;
40
41
mod webfw_iron;

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

47
use config::Config;
48
49
use structopt::StructOpt;

50
use std::path::{Path, PathBuf};
51

52
fn read_contest(p: &PathBuf) -> Option<Contest> {
53
54
    use std::fs::File;
    use std::io::Read;
55

56
57
58
    let mut file = File::open(p).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
59

60
    contestreader_yaml::parse_yaml(&contents,
61
62
                                   p.file_name().to_owned()?.to_str()?,
                                   &format!("{}/", p.parent().unwrap().to_str()?))
Robert Czechowski's avatar
Robert Czechowski committed
63
64
65
}

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

74
        if p.file_name().unwrap().to_string_lossy().to_string().ends_with(".yaml") {
75
            read_contest(p).map(|contest| contests.push(contest));
76
        };
Robert Czechowski's avatar
Robert Czechowski committed
77
78
79
    };

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

    contests
}

92
93
94
95
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
96
97
98
99
100
101
102
    let v = get_all_contest_info("tasks/");

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

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

Robert Czechowski's avatar
Robert Czechowski committed
119
    use rand::{distributions::Alphanumeric, thread_rng, Rng};
120
121

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

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

140
141
142
143
144
145
146
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) {
147
        print!("Scanning for contests …");
148
        refresh_all_contests(&mut conn);
149
        println!(" Done")
150
151
152
153
154
    }

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

155
        #[cfg(feature = "webbrowser")]
156
        let self_url = config.self_url.clone();
157
        #[cfg(feature = "webbrowser")]
158
159
        let open_browser = config.open_browser;

160
        match start_server(conn, config) {
161
162
            Ok(_) => {
                println!("Server started");
163

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

174
175
176
177
        println!("Could not run server. Is the port already in use?");
    }
}

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

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

190
    let mut config = config::read_config_from_file(&opt.configfile);
191

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

211
212
213
214
215
216
217
218
219
220
    #[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;
        }
221
    }
222
223
224
225
226
227
228
229
230
231
232
233
234
235

    #[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.");
236
}
237
238
239
240

#[cfg(test)]
mod tests {
    use super::*;
Robert Czechowski's avatar
Robert Czechowski committed
241
    use reqwest::StatusCode;
242

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

        thread::spawn(move || {
251
            let mut conn = rusqlite::Connection::open_in_memory().unwrap();
252
253
            db_apply_migrations::test(&mut conn);

254
            if let Some(user) = set_user {
255
                let mut test_user = conn.new_session("");
256
                test_user.username = Some(user.0);
Robert Czechowski's avatar
Robert Czechowski committed
257
258
259
                test_user.is_teacher = user.2;
                test_user.set_password(&user.1).expect("Set Password did not work correctly.");
                conn.save_session(test_user);
260
261
            }

262
            let mut config = config::read_config_from_file(Path::new("thisfileshoudnotexist"));
263
264
265
            config.port = Some(port);
            let srvr = start_server(conn, config);

266
            // Message server started
267
268
            start_tx.send(()).unwrap();

269
            // Wait for test to finish
270
271
272
273
274
            stop_rx.recv().unwrap();

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

275
        // Wait for server to start
276
277
278
        start_rx.recv().unwrap();
        thread::sleep(time::Duration::from_millis(100));
        f();
279
        // Message test finished
280
281
282
        stop_tx.send(()).unwrap();
    }

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

295
    #[test]
Robert Czechowski's avatar
Robert Czechowski committed
296
    fn start_server_and_check_requests() {
Robert Czechowski's avatar
Robert Czechowski committed
297
        start_server_and_fn(8080, None, || {
298
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
299
            assert_eq!(resp.status(), StatusCode::OK);
300
301

            let content = resp.text().unwrap();
302
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
303
            assert!(!content.contains("Error"));
Robert Czechowski's avatar
Robert Czechowski committed
304
            assert!(!content.contains("Gruppenverwaltung"));
305
306

            let mut resp = reqwest::get("http://localhost:8080/contest").unwrap();
307
            assert_eq!(resp.status(), StatusCode::OK);
308
309

            let content = resp.text().unwrap();
310
311
            assert!(content.contains("<h1>Wettbewerbe</h1>"));
            assert!(!content.contains("Error"));
Robert Czechowski's avatar
Robert Czechowski committed
312
313

            let mut resp = reqwest::get("http://localhost:8080/group").unwrap();
314
            let content = resp.text().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
315
            assert!(content.contains("<h1>Login</h1>"));
316
317
        })
    }
Daniel Brüning's avatar
Daniel Brüning committed
318

319
320
    #[test]
    fn check_login_wrong_credentials() {
Robert Czechowski's avatar
Robert Czechowski committed
321
        start_server_and_fn(8081, None, || {
322
            let client = reqwest::Client::new();
323
            let mut resp = login(8081, &client, "nonexistingusername", "wrongpassword");
324
            assert_eq!(resp.status(), StatusCode::OK);
325
326

            let content = resp.text().unwrap();
327
328
            assert!(content.contains("<h1>Login</h1>"));
            assert!(content.contains("Login fehlgeschlagen."));
329
            assert!(!content.contains("Error"));
330
        })
331
    }
332
333
334

    #[test]
    fn start_server_and_check_login() {
Robert Czechowski's avatar
Robert Czechowski committed
335
        start_server_and_fn(8082, Some(("testusr".to_string(), "testpw".to_string(), false)), || {
336
337
338
339
            let client = reqwest::Client::builder().cookie_store(true)
                                                   .redirect(reqwest::RedirectPolicy::none())
                                                   .build()
                                                   .unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
340

341
            let mut resp = login(8082, &client, "testusr", "testpw");
342
            assert_eq!(resp.status(), StatusCode::FOUND);
343

344
            let content = resp.text().unwrap();
345
346
            assert!(!content.contains("Error"));

347
348
349
350
351
352
353
            let mut set_cookie = resp.headers().get_all("Set-Cookie").iter();
            assert!(set_cookie.next().is_some());
            assert!(set_cookie.next().is_none());

            let mut resp = client.get("http://localhost:8082").send().unwrap();
            assert_eq!(resp.status(), StatusCode::OK);

354
            let content = resp.text().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
355
356
357
358
            assert!(!content.contains("Error"));
            assert!(!content.contains("Gruppenverwaltung"));
            assert!(content.contains("Eingeloggt als <em>testusr</em>"));
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
359
360
361
        })
    }

362
363
    #[test]
    fn start_server_and_check_logout() {
Robert Czechowski's avatar
Robert Czechowski committed
364
        start_server_and_fn(8083, Some(("testusr".to_string(), "testpw".to_string(), false)), || {
365
366
367
368
            let client = reqwest::Client::builder().cookie_store(true)
                                                   .redirect(reqwest::RedirectPolicy::none())
                                                   .build()
                                                   .unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
369

370
            let resp = login(8083, &client, "testusr", "testpw");
371
372
373
374
375
376
377
378
379
380
381
            assert_eq!(resp.status(), StatusCode::FOUND);

            let mut set_cookie = resp.headers().get_all("Set-Cookie").iter();
            assert!(set_cookie.next().is_some());
            assert!(set_cookie.next().is_none());

            let resp = client.get("http://localhost:8083/logout").send().unwrap();
            assert_eq!(resp.status(), StatusCode::FOUND);

            let mut resp = client.get("http://localhost:8083").send().unwrap();
            assert_eq!(resp.status(), StatusCode::OK);
382
383

            let content = resp.text().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
384
385
386
387
            assert!(content.contains("Benutzername"));
            assert!(content.contains("Passwort"));
            assert!(content.contains("Gruppencode / Teilnahmecode"));
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
388
389
390
        })
    }

391
392
    #[test]
    fn check_group_creation_and_group_code_login() {
Robert Czechowski's avatar
Robert Czechowski committed
393
        start_server_and_fn(8084, Some(("testusr".to_string(), "testpw".to_string(), true)), || {
394
395
396
397
398
            let client = reqwest::Client::builder().cookie_store(true)
                                                   .redirect(reqwest::RedirectPolicy::none())
                                                   .build()
                                                   .unwrap();

399
            let resp = login(8084, &client, "testusr", "testpw");
400
            assert_eq!(resp.status(), StatusCode::FOUND);
401

402
403
404
            let mut set_cookie = resp.headers().get_all("Set-Cookie").iter();
            assert!(set_cookie.next().is_some());
            assert!(set_cookie.next().is_none());
405

406
407
            let mut resp = client.get("http://localhost:8084").send().unwrap();
            assert_eq!(resp.status(), StatusCode::OK);
408
409

            let content = resp.text().unwrap();
410
411
            assert!(content.contains("[Lehrer]"));
            assert!(content.contains("Gruppenverwaltung"));
412
413
414

            let mut resp = client.get("http://localhost:8084/group/").send().unwrap();
            assert_eq!(resp.status(), StatusCode::OK);
415
416

            let content = resp.text().unwrap();
417
418
            assert!(content.contains("Gruppe anlegen"));

419
            let params = [("name", "WrongGroupname"), ("tag", "WrongMarker"), ("csrf", "76CfTPJaoz")];
420
421
            let resp = client.post("http://localhost:8084/group/").form(&params).send().unwrap();
            assert_eq!(resp.status(), StatusCode::FORBIDDEN);
422
423

            let pos = content.find("type=\"hidden\" name=\"csrf\" value=\"").expect("CSRF-Token not found");
424
            let csrf = &content[pos + 33..pos + 43];
425
            let params = [("name", "Groupname"), ("tag", "Marker"), ("csrf", csrf)];
426
427
            let resp = client.post("http://localhost:8084/group/").form(&params).send().unwrap();
            assert_eq!(resp.status(), StatusCode::FOUND);
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462

            let mut resp = client.get("http://localhost:8084/group/").send().unwrap();
            let content = resp.text().unwrap();
            assert!(!content.contains("WrongGroupname"));
            
            let pos = content.find("<td><a href=\"/group/1\">Groupname</a></td>").expect("Group not found");
            let groupcode = &content[pos + 58..pos + 65];

            // New client to test group code login
            let client = reqwest::Client::builder().cookie_store(true)
                .redirect(reqwest::RedirectPolicy::none())
                .build()
                .unwrap();

            let resp = login_code(8084, &client, groupcode);
            assert_eq!(resp.status(), StatusCode::FOUND);

            let mut resp = client.get(resp.headers().get(reqwest::header::LOCATION).unwrap().to_str().unwrap()).send().unwrap();
            let content = resp.text().unwrap();

            let pos = content.find("<p>Login-Code: ").expect("Logincode not found");
            let logincode = &content[pos + 15..pos + 24];

            // New client to test login code login
            let client = reqwest::Client::builder().cookie_store(true)
                .redirect(reqwest::RedirectPolicy::none())
                .build()
                .unwrap();

            let resp = login_code(8084, &client, logincode);
            assert_eq!(resp.status(), StatusCode::FOUND);

            let mut resp = client.get(resp.headers().get(reqwest::header::LOCATION).unwrap().to_str().unwrap()).send().unwrap();
            let content = resp.text().unwrap();
            assert!(content.contains("Eingeloggt als <em></em>"));
463
464
        })
    }
465
}