main.rs 13.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

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
}