main.rs 15.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
extern crate postgres;
Robert Czechowski's avatar
Robert Czechowski committed
16
extern crate rand;
17
extern crate reqwest;
Robert Czechowski's avatar
Robert Czechowski committed
18
19
extern crate rusqlite;
extern crate serde_json;
20
extern crate serde_yaml;
Robert Czechowski's avatar
Robert Czechowski committed
21
22
23
24
extern crate staticfile;
extern crate structopt;
extern crate time;
extern crate urlencoded;
25
extern crate webbrowser;
26
27
28

mod db_apply_migrations;
mod db_conn;
29
mod db_conn_postgres;
30
mod db_conn_sqlite;
31
32
33
mod db_objects;

use db_conn::{MedalConnection, MedalObject};
Robert Czechowski's avatar
Robert Czechowski committed
34
use functions::SetPassword; // TODO: Refactor, so we don't need to take this from there!
35
36
37

use db_objects::*;

38
mod configreader_yaml;
Robert Czechowski's avatar
Robert Czechowski committed
39
mod webfw_iron;
Robert Czechowski's avatar
Robert Czechowski committed
40
41
42
43
44
45

use webfw_iron::start_server;

mod functions;

use std::fs;
Robert Czechowski's avatar
Robert Czechowski committed
46
use std::path;
Robert Czechowski's avatar
Robert Czechowski committed
47

Robert Czechowski's avatar
Robert Czechowski committed
48
use std::path::{Path, PathBuf};
49
50
use structopt::StructOpt;

51
52
53
mod oauth_provider;

#[derive(Serialize, Deserialize, Clone, Default, Debug)]
54
pub struct Config {
55
56
57
    host: Option<String>,
    port: Option<u16>,
    self_url: Option<String>,
58
    oauth_providers: Option<Vec<oauth_provider::OauthProvider>>,
59
    database_file: Option<PathBuf>,
60
    database_url: Option<String>,
61
    template: Option<String>,
62
    no_contest_scan: Option<bool>,
63
    open_browser: Option<bool>,
64
65
}

66
fn read_config_from_file(file: &Path) -> Config {
67
    use std::io::Read;
Daniel Brüning's avatar
Daniel Brüning committed
68

69
    println!("Reading configuration file '{}'", file.to_str().unwrap_or("<Encoding error>"));
Daniel Brüning's avatar
Daniel Brüning committed
70

Robert Czechowski's avatar
Robert Czechowski committed
71
    let mut config: Config = if let Ok(mut file) = fs::File::open(file) {
72
73
74
75
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();
        serde_json::from_str(&contents).unwrap()
    } else {
76
        println!("Configuration file '{}' not found.", file.to_str().unwrap_or("<Encoding error>"));
77
        Default::default()
78
79
    };

80
81
82
83
84
85
86
    if let Some(ref oap) = config.oauth_providers {
        println!("OAuth providers:");
        for oap in oap {
            println!("  * {}", oap.provider_id);
        }
    }

Robert Czechowski's avatar
Robert Czechowski committed
87
88
89
90
91
92
93
94
95
    if config.host.is_none() {
        config.host = Some("[::]".to_string())
    }
    if config.port.is_none() {
        config.port = Some(8080)
    }
    if config.self_url.is_none() {
        config.self_url = Some("http://localhost:8080".to_string())
    }
96
97
98
    if config.template.is_none() {
        config.template = Some("default".to_string())
    }
99
100
101
    if config.no_contest_scan.is_none() {
        config.no_contest_scan = Some(false)
    }
102
103
104
    if config.open_browser.is_none() {
        config.open_browser = Some(false)
    }
105

106
    println!("OAuth providers will be told to redirect to {}", config.self_url.as_ref().unwrap());
107

108
    config
109
}
110

111
112
113
114
115
116
117
118
119
120
#[derive(StructOpt, Debug)]
#[structopt()]
struct Opt {
    /// Config file to use (default: 'config.json')
    #[structopt(short = "c", long = "config", default_value = "config.json", parse(from_os_str))]
    configfile: PathBuf,

    /// Database file to use (default: from config file or 'medal.db')
    #[structopt(short = "d", long = "database", parse(from_os_str))]
    databasefile: Option<PathBuf>,
121

122
123
124
125
    /// Database file to use (default: from config file or 'medal.db')
    #[structopt(short = "D", long = "databaseurl")]
    databaseurl: Option<String>,

126
127
128
    /// Port to listen on (default: from config file or 8080)
    #[structopt(short = "p", long = "port")]
    port: Option<u16>,
129
130
131
132

    /// Reset password of admin user (user_id=1)
    #[structopt(short = "a", long = "reset-admin-pw")]
    resetadminpw: bool,
133
134
135
136
137
138
139
140

    /// Run medal without scanning for contests
    #[structopt(short = "S", long = "no-contest-scan")]
    nocontestscan: bool,

    /// Scan for contests without starting medal
    #[structopt(short = "s", long = "only-contest-scan")]
    onlycontestscan: bool,
141
142
143
144

    /// Automatically open medal in the default browser
    #[structopt(short = "b", long = "browser")]
    openbrowser: bool,
145
146
}

Robert Czechowski's avatar
Robert Czechowski committed
147
fn read_contest(p: &path::PathBuf) -> Option<Contest> {
148
149
    use std::fs::File;
    use std::io::Read;
150

151
152
153
    let mut file = File::open(p).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
154

Robert Czechowski's avatar
Robert Czechowski committed
155
156
157
    configreader_yaml::parse_yaml(&contents,
                                  p.file_name().to_owned()?.to_str()?,
                                  &format!("{}/", p.parent().unwrap().to_str()?))
Robert Czechowski's avatar
Robert Czechowski committed
158
159
160
161
}

fn get_all_contest_info(task_dir: &str) -> Vec<Contest> {
    fn walk_me_recursively(p: &path::PathBuf, contests: &mut Vec<Contest>) {
162
163
        if let Ok(paths) = fs::read_dir(p) {
            for path in paths {
Robert Czechowski's avatar
Robert Czechowski committed
164
165
                let p = path.unwrap().path();
                walk_me_recursively(&p, contests);
166
            }
Robert Czechowski's avatar
Robert Czechowski committed
167
        }
168

169
        if p.file_name().unwrap().to_string_lossy().to_string().ends_with(".yaml") {
170
            read_contest(p).map(|contest| contests.push(contest));
171
        };
Robert Czechowski's avatar
Robert Czechowski committed
172
173
174
175
176
    };

    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
177
178
179
180
181
        Ok(paths) => {
            for path in paths {
                walk_me_recursively(&path.unwrap().path(), &mut contests);
            }
        }
Robert Czechowski's avatar
Robert Czechowski committed
182
183
184
185
186
    };

    contests
}

187
188
189
190
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
191
192
193
194
195
196
197
    let v = get_all_contest_info("tasks/");

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

198
199
fn add_admin_user<C>(conn: &mut C, resetpw: bool)
    where C: MedalConnection {
200
201
202
    let mut admin = match conn.get_user_by_id(1) {
        None => {
            print!("New Database. Creating new admin user with credentials 'admin':");
203
            conn.new_session("")
Robert Czechowski's avatar
Robert Czechowski committed
204
        }
205
206
        Some(user) => {
            if !resetpw {
Robert Czechowski's avatar
Robert Czechowski committed
207
                return;
208
            }
209
210
211
212
213
            print!("Request to reset admin password. Set credentials 'admin':");
            user
        }
    };

Robert Czechowski's avatar
Robert Czechowski committed
214
    use rand::{distributions::Alphanumeric, thread_rng, Rng};
215
216

    let password: String = thread_rng().sample_iter(&Alphanumeric)
Robert Czechowski's avatar
Robert Czechowski committed
217
218
219
220
221
222
                                       .filter(|x| {
                                           let x = *x;
                                           !(x == 'l' || x == 'I' || x == '1' || x == 'O' || x == 'o' || x == '0')
                                       })
                                       .take(8)
                                       .collect();
223
224
225
226
    print!("'{}' …", &password);

    admin.username = Some("admin".into());
    match admin.set_password(&password) {
227
        None => println!(" FAILED! (Password hashing error)"),
228
229
        _ => {
            conn.save_session(admin);
230
            println!(" Done");
231
        }
232
233
234
    }
}

235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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>
{
    println!("connected!");

    println!("applying migrations …");
    db_apply_migrations::test(&mut conn);

    if onlycontestscan || config.no_contest_scan == Some(false) {
        println!("scanning for contests …");
        refresh_all_contests(&mut conn);
        println!("finished")
    }

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

253
254
255
        let self_url = config.self_url.clone();
        let open_browser = config.open_browser;

256
        match start_server(conn, config) {
257
258
259
260
261
            Ok(_) => {
                println!("Server started");
                if let (Some(self_url), Some(true)) = (self_url, open_browser) {
                    open_browser_window(&self_url);
                }
262
            }
263
264
            Err(_) => println!("Error on server start …"),
        };
265

266
267
268
269
        println!("Could not run server. Is the port already in use?");
    }
}

270
271
272
fn open_browser_window(self_url: &str) {
    match webbrowser::open(&self_url) {
        Ok(_) => (),
273
        Err(e) => println!("Error while opening webbrowser: {:?}", e),
274
275
276
    }
}

Robert Czechowski's avatar
Robert Czechowski committed
277
fn main() {
278
    let opt = Opt::from_args();
279
    //println!("{:?}", opt); // Show in different debug level?
Daniel Brüning's avatar
Daniel Brüning committed
280

281
282
    let mut config = read_config_from_file(&opt.configfile);

Robert Czechowski's avatar
Robert Czechowski committed
283
284
285
    if opt.databasefile.is_some() {
        config.database_file = opt.databasefile;
    }
286
287
288
    if opt.databaseurl.is_some() {
        config.database_url = opt.databaseurl;
    }
Robert Czechowski's avatar
Robert Czechowski committed
289
290
291
    if opt.port.is_some() {
        config.port = opt.port;
    }
292
293
294
    if opt.nocontestscan {
        config.no_contest_scan = Some(true);
    }
295
296
297
    if opt.openbrowser {
        config.open_browser = Some(true)
    }
298

299
300
301
    if config.database_url.is_some() {
        let conn =
            postgres::Connection::connect(config.database_url.clone().unwrap(), postgres::TlsMode::None).unwrap();
302

303
304
305
306
307
308
309
310
311
312
313
        prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw);
    } else {
        let conn = match config.database_file {
            Some(ref path) => {
                println!("Using database file {}", &path.to_str().unwrap_or("<unprintable filename>"));
                rusqlite::Connection::open(path).unwrap()
            }
            None => {
                println!("Using default database file ./medal.db");
                rusqlite::Connection::open(&Path::new("medal.db")).unwrap()
            }
314
        };
Robert Czechowski's avatar
Robert Czechowski committed
315

316
        prepare_and_start_server(conn, config, opt.onlycontestscan, opt.resetadminpw);
317
    }
318
}
319
320
321
322

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

Robert Czechowski's avatar
Robert Czechowski committed
325
326
    fn start_server_and_fn<F>(port: u16, set_user: Option<(String, String)>, f: F)
        where F: FnOnce() {
327
        use std::sync::mpsc::channel;
Robert Czechowski's avatar
Robert Czechowski committed
328
        use std::{thread, time};
329
330
331
332
        let (start_tx, start_rx) = channel();
        let (stop_tx, stop_rx) = channel();

        thread::spawn(move || {
333
            let mut conn = rusqlite::Connection::open_in_memory().unwrap();
334
335
            db_apply_migrations::test(&mut conn);

336
            if let Some(user) = set_user {
337
                let mut test_user = conn.new_session("");
338
                test_user.username = Some(user.0);
339
340
341
                match test_user.set_password(&user.1) {
                    None => panic!("Set Password did not work correctly.)"),
                    _ => conn.save_session(test_user),
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
                }
            }

            let mut config = read_config_from_file(Path::new("thisfileshoudnotexist"));
            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();
    }

363
    fn login_for_tests(port: u16, client: &reqwest::Client, username: &str, password: &str) -> reqwest::Response {
364
        let params = [("username", username), ("password", password)];
Robert Czechowski's avatar
Robert Czechowski committed
365
        let resp = client.post(&format!("http://localhost:{}/login", port)).form(&params).send().unwrap();
366
        resp
367
368
    }

369
370
371
372
373
374
375
    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)
        };
    }

376
377
    #[test]
    fn start_server_and_check_request() {
Robert Czechowski's avatar
Robert Czechowski committed
378
        start_server_and_fn(8080, None, || {
379
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
380
            check_status(&resp, reqwest::StatusCode::Ok);
381
            let mut content = String::new();
382
            resp.read_to_string(&mut content).unwrap();
383
            assert!(content.contains("Jugendwettbewerb Informatik</h1>"));
384
385
386
            assert!(!content.contains("Error"));

            let mut resp = reqwest::get("http://localhost:8080/contest").unwrap();
387
            check_status(&resp, reqwest::StatusCode::Ok);
388
            let mut content = String::new();
389
            resp.read_to_string(&mut content).unwrap();
390
391
392
393
            assert!(content.contains("<h1>Wettbewerbe</h1>"));
            assert!(!content.contains("Error"));
        })
    }
Daniel Brüning's avatar
Daniel Brüning committed
394

395
396
    #[test]
    fn check_login_wrong_credentials() {
Robert Czechowski's avatar
Robert Czechowski committed
397
        start_server_and_fn(8081, None, || {
398
            let client = reqwest::Client::new().unwrap();
399
400
            let mut resp = login_for_tests(8081, &client, "nonexistingusername", "wrongpassword");
            check_status(&resp, reqwest::StatusCode::Ok);
401
            let mut content = String::new();
402
            resp.read_to_string(&mut content).unwrap();
403
404
            assert!(content.contains("<h1>Login</h1>"));
            assert!(content.contains("Login fehlgeschlagen."));
405
            assert!(!content.contains("Error"));
406
        })
407
    }
408
409
410

    #[test]
    fn start_server_and_check_login() {
Robert Czechowski's avatar
Robert Czechowski committed
411
        start_server_and_fn(8082, Some(("testusr".to_string(), "testpw".to_string())), || {
412
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
413
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
414
415
416
            let mut resp = login_for_tests(8082, &client, "testusr", "testpw");
            check_status(&resp, reqwest::StatusCode::Found);

417
            let mut content = String::new();
418
            resp.read_to_string(&mut content).unwrap();
419
420
421
            assert!(!content.contains("Error"));

            let header = resp.headers();
422
423
            let set_cookie = header.get::<reqwest::header::SetCookie>();
            match set_cookie {
424
                None => panic!("No setCookie."),
Robert Czechowski's avatar
Robert Czechowski committed
425
426
                Some(cookie) => {
                    if cookie.len() == 1 {
427
                        let new_cookie = reqwest::header::Cookie(cookie.to_vec());
Robert Czechowski's avatar
Robert Czechowski committed
428
                        let mut new_resp = client.get("http://localhost:8082").header(new_cookie).send().unwrap();
429
430
431
432
433
434
                        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>"));
435
                        assert!(new_content.contains("Jugendwettbewerb Informatik</h1>"));
436
437
                    } else {
                        panic!("More than one setCookie.");
Robert Czechowski's avatar
Robert Czechowski committed
438
439
                    }
                }
440
441
442
443
            };
        })
    }

444
445
    #[test]
    fn start_server_and_check_logout() {
Robert Czechowski's avatar
Robert Czechowski committed
446
        start_server_and_fn(8083, Some(("testusr".to_string(), "testpw".to_string())), || {
447
            let mut client = reqwest::Client::new().unwrap();
Robert Czechowski's avatar
Robert Czechowski committed
448
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| attempt.stop()));
449
            let resp = login_for_tests(8083, &client, "testusr", "testpw");
450
            check_status(&resp, reqwest::StatusCode::Found);
451
452

            let header = resp.headers();
453
454
            let set_cookie = header.get::<reqwest::header::SetCookie>();
            match set_cookie {
455
                None => panic!("No setCookie."),
Robert Czechowski's avatar
Robert Czechowski committed
456
457
458
459
460
461
462
463
464
465
466
                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();
467
468
469
470
                        assert!(new_content.contains("Benutzername"));
                        assert!(new_content.contains("Passwort"));
                        assert!(new_content.contains("Gruppencode / Teilnahmecode"));
                        assert!(new_content.contains("Jugendwettbewerb Informatik</h1>"));
471
472
                    } else {
                        panic!("More than one setCookie.");
Robert Czechowski's avatar
Robert Czechowski committed
473
474
                    }
                }
475
476
477
478
            };
        })
    }

479
}