main.rs 11.2 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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
    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") {
                None => println!("FAILED! (Password hashing error)"),
                _ => {
                    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
288
    #[test]
    fn start_server_and_check_request() {
289
        start_server_and_fn(8080, ||{
290
            let mut resp = reqwest::get("http://localhost:8080").unwrap();
Daniel Brüning's avatar
Daniel Brüning committed
291
            assert!(resp.status().is_success());
292
293
294
295
296
297
298
            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();
299
            assert!(resp.status().is_success());
300
301
302
303
304
305
            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
306

307
308
309
310
311
312
313
    #[test]
    fn check_login_wrong_credentials() {
        start_server_and_fn(8081, ||{
            let params = [("username", "nonexistingusername"), ("password", "wrongpassword")];
            let client = reqwest::Client::new().unwrap();
            let mut resp = client.post("http://localhost:8081/login")
                .form(&params).send().unwrap();
314
315
            let mut content = String::new();
            resp.read_to_string(&mut content);
316
317
            assert!(content.contains("<h1>Login</h1>"));
            assert!(content.contains("Login fehlgeschlagen."));
318
            assert!(!content.contains("Error"));
319
        })
320
    }
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355

    #[test]
    fn start_server_and_check_login() {
        start_server_and_create_user_and_fn(8082, ||{
            let params = [("username", "testusr"), ("password", "testpw")];
            let mut client = reqwest::Client::new().unwrap();
            client.redirect(reqwest::RedirectPolicy::custom(|attempt| {attempt.stop()}));
            let mut resp = client.post("http://localhost:8082/login")
                .form(&params).send().unwrap();
            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());
                    println!("newCookie: {:?}", newCookie);
                    let mut newResp = client.get("http://localhost:8082")
                        .header(newCookie).send().unwrap();

                    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.");
                    },
            };
        })
    }

356
}