main.rs 15.1 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;
Robert Czechowski's avatar
Robert Czechowski committed
245
    use reqwest::StatusCode;
246

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

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

258
            if let Some(user) = set_user {
259
                let mut test_user = conn.new_session("");
260
                test_user.username = Some(user.0);
Robert Czechowski's avatar
Robert Czechowski committed
261
262
263
                test_user.is_teacher = user.2;
                test_user.set_password(&user.1).expect("Set Password did not work correctly.");
                conn.save_session(test_user);
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
    #[test]
Robert Czechowski's avatar
Robert Czechowski committed
291
    fn start_server_and_check_requests() {
Robert Czechowski's avatar
Robert Czechowski committed
292
        start_server_and_fn(8080, None, || {
293
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
294
            assert_eq!(resp.status(), &StatusCode::Ok);
295
            let mut content = String::new();
296
            resp.read_to_string(&mut content).unwrap();
297
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
298
            assert!(!content.contains("Error"));
Robert Czechowski's avatar
Robert Czechowski committed
299
            assert!(!content.contains("Gruppenverwaltung"));
300
301

            let mut resp = reqwest::get("http://localhost:8080/contest").unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
302
            assert_eq!(resp.status(), &StatusCode::Ok);
303
            let mut content = String::new();
304
            resp.read_to_string(&mut content).unwrap();
305
306
            assert!(content.contains("<h1>Wettbewerbe</h1>"));
            assert!(!content.contains("Error"));
Robert Czechowski's avatar
Robert Czechowski committed
307
308
309
310
311

            let mut resp = reqwest::get("http://localhost:8080/group").unwrap();
            let mut content = String::new();
            resp.read_to_string(&mut content).unwrap();
            assert!(content.contains("<h1>Login</h1>"));
312
313
        })
    }
Daniel Brüning's avatar
Daniel Brüning committed
314

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

    #[test]
    fn start_server_and_check_login() {
Robert Czechowski's avatar
Robert Czechowski committed
331
        start_server_and_fn(8082, Some(("testusr".to_string(), "testpw".to_string(), false)), || {
332
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
333
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
Robert Czechowski's avatar
Robert Czechowski committed
334

335
            let mut resp = login_for_tests(8082, &client, "testusr", "testpw");
Robert Czechowski's avatar
Robert Czechowski committed
336
            assert_eq!(resp.status(), &StatusCode::Found);
337

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();
Robert Czechowski's avatar
Robert Czechowski committed
343
344
345
346
347
348
349
350
351
352
353
354
355
            let set_cookie = header.get::<reqwest::header::SetCookie>().expect("No coockies transmitted");
            assert_eq!(set_cookie.len(), 1);
            
            let cookie = reqwest::header::Cookie(set_cookie.to_vec());
            let mut resp = client.get("http://localhost:8082").header(cookie).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Ok);
            
            let mut content = String::new();
            resp.read_to_string(&mut content).unwrap();
            assert!(!content.contains("Error"));
            assert!(!content.contains("Gruppenverwaltung"));
            assert!(content.contains("Eingeloggt als <em>testusr</em>"));
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
356
357
358
        })
    }

359
360
    #[test]
    fn start_server_and_check_logout() {
Robert Czechowski's avatar
Robert Czechowski committed
361
        start_server_and_fn(8083, Some(("testusr".to_string(), "testpw".to_string(), false)), || {
362
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
363
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
Robert Czechowski's avatar
Robert Czechowski committed
364

365
            let resp = login_for_tests(8083, &client, "testusr", "testpw");
Robert Czechowski's avatar
Robert Czechowski committed
366
            assert_eq!(resp.status(), &StatusCode::Found);
367
368

            let header = resp.headers();
Robert Czechowski's avatar
Robert Czechowski committed
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
            let set_cookie = header.get::<reqwest::header::SetCookie>().expect("No coockies transmitted");
            assert_eq!(set_cookie.len(), 1);
            let cookie = reqwest::header::Cookie(set_cookie.to_vec());

            let resp = client.get("http://localhost:8083/logout").header(cookie.clone()).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Found);
            
            let mut resp = client.get("http://localhost:8083").header(cookie).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Ok);
            let mut content = String::new();
            resp.read_to_string(&mut content).unwrap();
            assert_eq!(resp.status(), &StatusCode::Ok);
            assert!(content.contains("Benutzername"));
            assert!(content.contains("Passwort"));
            assert!(content.contains("Gruppencode / Teilnahmecode"));
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
385
386
387
        })
    }

388
389
    #[test]
    fn check_group_creation_and_group_code_login() {
Robert Czechowski's avatar
Robert Czechowski committed
390
        start_server_and_fn(8084, Some(("testusr".to_string(), "testpw".to_string(), true)), || {
391
392
            let mut client = reqwest::Client::new().unwrap();
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
Robert Czechowski's avatar
Robert Czechowski committed
393
            
394
            let resp = login_for_tests(8084, &client, "testusr", "testpw");
Robert Czechowski's avatar
Robert Czechowski committed
395
            assert_eq!(resp.status(), &StatusCode::Found);
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425

            let header = resp.headers();
            let set_cookie = header.get::<reqwest::header::SetCookie>().expect("No coockies transmitted");
            assert_eq!(set_cookie.len(), 1);
            let cookie = reqwest::header::Cookie(set_cookie.to_vec());

            let mut resp = client.get("http://localhost:8084").header(cookie.clone()).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Ok);
            let mut content = String::new();
            resp.read_to_string(&mut content).unwrap();
            assert!(content.contains("[Lehrer]"));
            assert!(content.contains("Gruppenverwaltung"));
            
            
            let mut resp = client.get("http://localhost:8084/group/").header(cookie.clone()).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Ok);
            let mut content = String::new();
            resp.read_to_string(&mut content).unwrap();
            assert!(content.contains("Gruppe anlegen"));

            let params = [("name", "groupname"), ("tag", "marker"), ("csrf", "76CfTPJaoz")];
            let resp = client.post("http://localhost:8084/group/").form(&params).header(cookie.clone()).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Forbidden);

            let pos = content.find("type=\"hidden\" name=\"csrf\" value=\"").expect("CSRF-Token not found");
            let csrf = &content[pos+33..pos+43];

            let params = [("name", "groupname"), ("tag", "marker"), ("csrf", csrf)];
            let resp = client.post("http://localhost:8084/group/").form(&params).header(cookie).send().unwrap();
            assert_eq!(resp.status(), &StatusCode::Found);
426
427
428
        })
    }

429
}