#![allow(unreachable_code)] #[macro_use] extern crate rouille; use rouille::Request; use rouille::Response; use std::collections::HashMap; use std::io; use std::sync::Mutex; // This struct contains the data that we store on the server about each client. #[derive(Debug, Clone)] struct SessionData { login: String, } fn main() { // This example demonstrates how to create a website with a simple login form. // Small message so that people don't need to read the source code. // Note that like all examples we only listen on `localhost`, so you can't access this server // from another machine than your own. println!("Now listening on localhost:8000"); // For the sake of the example, we are going to store the sessions data in a hashmap in memory. // This has the disadvantage that all the sessions are erased if the program reboots (for // example because of an update), and that if you start multiple processes of the same // application (for example for load balancing) then they won't share sessions. // Therefore in a real project you should store probably the sessions in a database of some // sort instead. // // We created a struct that contains the data that we store on the server for each session, // and a hashmap that associates each session ID with the data. let sessions_storage: Mutex> = Mutex::new(HashMap::new()); rouille::start_server("localhost:8000", move |request| { rouille::log(&request, io::stdout(), || { // We call `session::session` in order to assign a unique identifier to each client. // This identifier is tracked through a cookie that is automatically appended to the // response. // // The parameters of the function are the name of the cookie (here "SID") and the // duration of the session in seconds (here, one hour). rouille::session::session(request, "SID", 3600, |session| { // If the client already has an identifier from a previous request, we try to load // the existing session data. If we successfully load data from `sessions_storage`, // we make a copy of the data in order to avoid locking the session for too long. // // We thus obtain a `Option`. let mut session_data = if session.client_has_sid() { if let Some(data) = sessions_storage.lock().unwrap().get(session.id()) { Some(data.clone()) } else { None } } else { None }; // Use a separate function to actually handle the request, for readability. // We pass a mutable reference to the `Option` so that the function // is free to modify it. let response = handle_route(&request, &mut session_data); // Since the function call to `handle_route` can modify the session data, we have // to store it back in the `sessions_storage` when necessary. if let Some(d) = session_data { sessions_storage .lock() .unwrap() .insert(session.id().to_owned(), d); } else if session.client_has_sid() { // If `handle_route` erased the content of the `Option`, we remove the session // from the storage. This is only done if the client already has an identifier, // otherwise calling `session.id()` will assign one. sessions_storage.lock().unwrap().remove(session.id()); } // During the whole handling of the request, the `sessions_storage` mutex was only // briefly locked twice. This shouldn't have a lot of influence on performances. response }) }) }); } // This is the function that truly handles the routes. // // The `session_data` parameter holds what we know about the client. It can be modified by the // body of this function. Keep in my mind that the way we designed `session_data` is appropriate // for most situations but not all. If for example you want to keep track of the pages that the // user visited, you should design it in another way, otherwise the data of some requests will // overwrite the data of other requests. fn handle_route(request: &Request, session_data: &mut Option) -> Response { // First we handle the routes that are always accessible and always the same, no matter whether // the user is logged in or not. router!(request, (POST) (/login) => { // This is the route that is called when the user wants to log in. // In order to retrieve what the user sent us through the
, we use the // `post_input!` macro. This macro returns an error (if a field is missing for example), // so we use the `try_or_400!` macro to handle any possible error. // // If the macro is successful, `data` is an instance of a struct that has one member // for each field that we indicated in the macro. let data = try_or_400!(post_input!(request, { login: String, password: String, })); // Just a small debug message for this example. You could also output something in the // logs in a real application. println!("Login attempt with login {:?} and password {:?}", data.login, data.password); // In this example all login attempts are successful in the password starts with the // letter 'b'. Of course in a real website you should check the credentials in a proper // way. if data.password.starts_with("b") { // Logging the user in is done by writing the content of `session_data`. // // A minor warning here: in this demo we store in memory directly the data that // the user gave us. This data is not to be trusted and could contain anything, // including an attempt at XSS. Storing in memory what the user gave us is not // wrong, but we have to take care not to interpret it as HTML data for example. *session_data = Some(SessionData { login: data.login }); return Response::redirect_303("/"); } else { // We return a dummy response to indicate that the login failed. In a real // application you should probably use some sort of HTML templating instead. return Response::html("Wrong login/password"); } }, (POST) (/logout) => { // This route is called when the user wants to log out. // We do so by simply erasing the content of `session_data`, which deletes the session. *session_data = None; // We return a dummy response to indicate what happened. In a real application you // should probably use some sort of HTML templating instead. return Response::html(r#"Logout successful. Click here to go to the home"#); }, _ => () ); // Now that we handled all the routes that are accessible in all circumstances, we check // that the user is logged in before proceeding. if let Some(session_data) = session_data.as_ref() { // Logged in. handle_route_logged_in(request, session_data) } else { // Not logged in. router!(request, (GET) (/) => { // When connecting to the root, show a login form. // Note that in a real website you should probably use some templating system, or // at least load the HTML from a file. Response::html(r#"

Hint: in this example all passwords that start with the letter 'b' (lowercase) are valid.

Or you can try going to the private area without logging in, but you will be redirected back here.

"#) }, _ => { // If the user tries to access any other route, redirect them to the login form. // // You may wonder: if I want to make some parts of my site public and some other // parts private, should I put all my public routes here? The answer is no. The way // this example is structured is appropriate for a website that is entirely // private. Don't hesitate to structure it in a different way, for example by // having a function that is dedicated only to public routes. Response::redirect_303("/") } ) } } // This function handles the routes that are accessible only if the user is logged in. fn handle_route_logged_in(request: &Request, _session_data: &SessionData) -> Response { router!(request, (GET) (/) => { // Show some greetings with a dummy response. Response::html(r#"You are now logged in. If you close your tab and open it again, you will still be logged in.
Click here for the private area
"#) }, (GET) (/private) => { // This route is here to demonstrate that the client can go to `/private` only if // they are successfully logged in. Response::html(r#"You are in the private area! Go back."#) }, _ => Response::empty_404() ) }