main.rs 13.3 KB
Newer Older
Robert Czechowski's avatar
Robert Czechowski committed
1
2
3
4
5
6
7
#[macro_use]
extern crate iron;
#[macro_use]
extern crate router;
#[macro_use]
extern crate serde_derive;

8
extern crate structopt;
9
extern crate rusqlite;
Robert Czechowski's avatar
Robert Czechowski committed
10
11
12
13
14
15
16
17
18
extern crate iron_sessionstorage;
extern crate urlencoded;
extern crate time;
extern crate persistent;
extern crate rand;
extern crate mount;
extern crate staticfile;
extern crate handlebars_iron;
extern crate serde_json;
19
20
21
extern crate params;
extern crate reqwest;
extern crate serde_yaml;
22
23
24
25
26
27
28
29

use rusqlite::Connection;

mod db_apply_migrations;
mod db_conn_sqlite;
mod db_conn;
mod db_objects;

30
use functions::SetPassword; // TODO: Refactor, so we don't need to take this from there!
31
32
33
34
use db_conn::{MedalConnection, MedalObject};

use db_objects::*;

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

use webfw_iron::start_server;

mod functions;

use std::path;
use std::fs;

45
46
47
use std::path::{Path,PathBuf};
use structopt::StructOpt;

48
#[derive(Serialize, Deserialize, Clone, Default)]
49
pub struct Config {
50
51
52
53
    host: Option<String>,
    port: Option<u16>,
    self_url: Option<String>,
    oauth_url: Option<String>,
54
55
56
57
    oauth_client_id: Option<String>,
    oauth_client_secret: Option<String>,
    oauth_access_token_url: Option<String>,
    oauth_user_data_url: Option<String>,
58
    database_file: Option<PathBuf>,
59
60
}

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

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

66
    let mut config : Config = if let Ok(mut file) = fs::File::open(file) {
67
68
69
70
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();
        serde_json::from_str(&contents).unwrap()
    } else {
71
        println!("Configuration file '{}' not found.", file.to_str().unwrap_or("<Encoding error>"));
72
        Default::default()
73
74
75
76
    };

    if config.host.is_none() {config.host = Some("[::]".to_string())}
    if config.port.is_none() {config.port = Some(8080)}
77
    if config.self_url.is_none() {config.self_url = Some("http://localhost:8080".to_string())}
78

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

81
    config
82
}
83

84
85
86
87
88
89
90
91
92
93
#[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>,
94
95
96
97

    /// Port to listen on (default: from config file or 8080)
    #[structopt(short = "p", long = "port")]
    port: Option<u16>,
98
99
100
101

    /// Reset password of admin user (user_id=1)
    #[structopt(short = "a", long = "reset-admin-pw")]
    resetadminpw: bool,
102
103
104
105
}



Robert Czechowski's avatar
Robert Czechowski committed
106
fn read_contest(p: &path::PathBuf) -> Option<Contest> {
107
108
    use std::fs::File;
    use std::io::Read;
109

110
111
112
    let mut file = File::open(p).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
113
114

    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
115
116
117
118
119
120
121
122
123
124
125
}

fn get_all_contest_info(task_dir: &str) -> Vec<Contest> {
    fn walk_me_recursively(p: &path::PathBuf, contests: &mut Vec<Contest>) {
        match fs::read_dir(p) {
            Ok(paths) => for path in paths {
                let p = path.unwrap().path();
                walk_me_recursively(&p, contests);
            },
            _ => (),
        }
126

127
        if p.file_name().unwrap().to_string_lossy().to_string().ends_with(".yaml") {
Robert Czechowski's avatar
Robert Czechowski committed
128
129
130
131
            match read_contest(p) {
                Some(contest) => contests.push(contest),
                _ => (),
            }
132
        };
Robert Czechowski's avatar
Robert Czechowski committed
133
134
    };

135

Robert Czechowski's avatar
Robert Czechowski committed
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
    let mut contests = Vec::new();
    match fs::read_dir(task_dir) {
        Err(why) => println!("Error opening tasks directory! {:?}", why.kind()),
        Ok(paths) => for path in paths {
            walk_me_recursively(&path.unwrap().path(), &mut contests);
        },
    };

    contests
}

fn refresh_all_contests(conn : &mut Connection) {
    let v = get_all_contest_info("tasks/");

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

155
156
157
158
159
fn add_admin_user(conn: &mut Connection, resetpw: bool) {
    let mut admin = match conn.get_user_by_id(1) {
        None => {
            print!("New Database. Creating new admin user with credentials 'admin':");
            conn.new_session()
Daniel Brüning's avatar
Daniel Brüning committed
160

161
162
163
164
        },
        Some(user) => {
            if !resetpw {
                return
165
            }
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
            print!("Request to reset admin password. Set credentials 'admin':");
            user
        }
    };

    use rand::{thread_rng, Rng, distributions::Alphanumeric};

    let password: String = thread_rng().sample_iter(&Alphanumeric)
        .filter(|x| {let x = *x; !(x == 'l' || x == 'I' || x == '1' || x == 'O' || x == 'o' || x == '0')})
        .take(8).collect();
    print!("'{}' …", &password);

    admin.username = Some("admin".into());
    match admin.set_password(&password) {
        None => println!("FAILED! (Password hashing error)"),
        _ => {
            conn.save_session(admin);
            println!("Done");
184
        }
185
186
187
    }
}

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

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

    if opt.databasefile.is_some() { config.database_file = opt.databasefile; }
195
    if opt.port.is_some() { config.port = opt.port; }
196

197
    let mut conn = match config.database_file {
198
        Some(ref path) => {println!("Using database file {}", &path.to_str().unwrap_or("<unprintable filename>"));  Connection::create(path)},
199
        None => {println!("Using default database file ./medal.db"); Connection::create(&Path::new("medal.db"))},
200
    };
201

Robert Czechowski's avatar
Robert Czechowski committed
202
203
204
    db_apply_migrations::test(&mut conn);

    refresh_all_contests(&mut conn);
205

206
207
    add_admin_user(&mut conn, opt.resetadminpw);

208
    match start_server(conn, config) {
209
210
        Ok(_) => println!("Server started"),
        Err(_) => println!("Error on server start …")
211
    };
Robert Czechowski's avatar
Robert Czechowski committed
212

213
    println!("Could not run server. Is the port already in use?");
214
}
215
216
217
218
219
220



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

223
    fn start_server_and_fn<F>(port: u16, f: F) where F: FnOnce() {
224
        use std::{thread, time};
225
226
227
        use std::sync::mpsc::channel;
        let (start_tx, start_rx) = channel();
        let (stop_tx, stop_rx) = channel();
228
229

        thread::spawn(move || {
230
231
232
            let mut conn = Connection::open_in_memory().unwrap();
            db_apply_migrations::test(&mut conn);
            let mut config = read_config_from_file(Path::new("thisfileshoudnotexist"));
233
            config.port = Some(port);
234
            let srvr = start_server(conn, config);
235

236
            start_tx.send(()).unwrap();
Daniel Brüning's avatar
Daniel Brüning committed
237

238
            stop_rx.recv().unwrap();
Daniel Brüning's avatar
Daniel Brüning committed
239
240

            srvr.unwrap().close().unwrap();
241
        });
Daniel Brüning's avatar
Daniel Brüning committed
242

243
244
245
246
        // wait for server to start:
        start_rx.recv().unwrap();
        thread::sleep(time::Duration::from_millis(100));
        f();
Daniel Brüning's avatar
Daniel Brüning committed
247
        stop_tx.send(()).unwrap();
248
    }
249

250
251
252
253
254
255
256
257
258
259
260
261
262
    fn start_server_and_create_user_and_fn<F>(port: u16, f: F) where F: FnOnce() {
        use std::{thread, time};
        use std::sync::mpsc::channel;
        let (start_tx, start_rx) = channel();
        let (stop_tx, stop_rx) = channel();

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

            let mut test_user = conn.new_session();
            test_user.username = Some("testusr".into());
            match test_user.set_password("testpw") {
263
                None => panic!("Set Password did not work correctly.)"),
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
                _ => {
                    conn.save_session(test_user);
                }
            }

            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();
    }

287
    fn login_for_tests(port: u16, client: &reqwest::Client, username: &str, password: &str) -> reqwest::Response {
288
289
290
291
292
293
        let params = [("username", username), ("password", password)];
        let mut resp = client.post(&format!("http://localhost:{}/login", port))
            .form(&params).send().unwrap();
        return resp;
    }

294
295
296
297
298
299
300
    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)
        };
    }

301
302
    #[test]
    fn start_server_and_check_request() {
303
        start_server_and_fn(8080, ||{
304
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
305
            check_status(&resp, reqwest::StatusCode::Ok);
306
307
308
309
310
311
312
            let mut content = String::new();
            resp.read_to_string(&mut content);
            assert!(content.contains("<h1>Jugendwettbewerb Informatik</h1>"));
            assert!(!content.contains("Error"));


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

321
322
323
324
    #[test]
    fn check_login_wrong_credentials() {
        start_server_and_fn(8081, ||{
            let client = reqwest::Client::new().unwrap();
325
326
            let mut resp = login_for_tests(8081, &client, "nonexistingusername", "wrongpassword");
            check_status(&resp, reqwest::StatusCode::Ok);
327
328
            let mut content = String::new();
            resp.read_to_string(&mut content);
329
330
            assert!(content.contains("<h1>Login</h1>"));
            assert!(content.contains("Login fehlgeschlagen."));
331
            assert!(!content.contains("Error"));
332
        })
333
    }
334
335
336
337
338
339

    #[test]
    fn start_server_and_check_login() {
        start_server_and_create_user_and_fn(8082, ||{
            let mut client = reqwest::Client::new().unwrap();
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| {attempt.stop()}));
340
341
342
            let mut resp = login_for_tests(8082, &client, "testusr", "testpw");
            check_status(&resp, reqwest::StatusCode::Found);

343
344
345
346
347
348
349
350
351
352
353
354
            let mut content = String::new();
            resp.read_to_string(&mut content);
            assert!(!content.contains("Error"));

            let header = resp.headers();
            let setCookie = header.get::<reqwest::header::SetCookie>();
            match setCookie {
                None => panic!("No setCookie."),
                Some(cookie) => if cookie.len() == 1 {
                    let newCookie = reqwest::header::Cookie(cookie.to_vec());
                    let mut newResp = client.get("http://localhost:8082")
                        .header(newCookie).send().unwrap();
355
                    check_status(&newResp, reqwest::StatusCode::Ok);
356
357
358
359
360
361
362
363
364
365
366
367
368

                    let mut newContent = String::new();
                    newResp.read_to_string(&mut newContent);
                    assert!(!content.contains("Error"));
                    assert!(newContent.contains("Eingeloggt als <em>testusr</em>"));
                    assert!(newContent.contains("<h1>Jugendwettbewerb Informatik</h1>"));
                    } else {
                        panic!("More than one setCookie.");
                    },
            };
        })
    }

369
370
371
372
373
    #[test]
    fn start_server_and_check_logout() {
        start_server_and_create_user_and_fn(8083, ||{
            let mut client = reqwest::Client::new().unwrap();
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| {attempt.stop()}));
374
375
            let mut resp = login_for_tests(8083, &client, "testusr", "testpw");
            check_status(&resp, reqwest::StatusCode::Found);
376
377
378
379
380
381
382
383

            let header = resp.headers();
            let setCookie = header.get::<reqwest::header::SetCookie>();
            match setCookie {
                None => panic!("No setCookie."),
                Some(cookie) => if cookie.len() == 1 {
                    let newCookie = reqwest::header::Cookie(cookie.to_vec());
                    let mut newResp = client.get("http://localhost:8082/logout")
Daniel Brüning's avatar
Daniel Brüning committed
384
                        .header(newCookie.clone()).send().unwrap();
385
                    check_status(&newResp, reqwest::StatusCode::Found);
386
                    newResp = client.get("http://localhost:8082")
Daniel Brüning's avatar
Daniel Brüning committed
387
                        .header(newCookie).send().unwrap();
388
                    check_status(&newResp, reqwest::StatusCode::Ok);
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403

                    let mut newContent = String::new();
                    newResp.read_to_string(&mut newContent);
                    assert!(newContent.contains("Benutzername:"));
                    assert!(newContent.contains("Passwort:"));
                    assert!(newContent.contains("Gruppencode / Teilnahmecode:"));
                    assert!(newContent.contains("<h1>Jugendwettbewerb Informatik</h1>"));
                    } else {
                        panic!("More than one setCookie.");
                    },
            };
        })
    }


404
}