rouille 2.1.0

High-level idiomatic web framework.
Documentation
#[macro_use]
extern crate rouille;

use std::collections::HashMap;
use std::io;
use std::sync::Mutex;
use rouille::Request;
use rouille::Response;

// 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<HashMap<String, SessionData>> = 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<SessionData>`.
                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<SessionData>` 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<SessionData>) -> 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 retreive what the user sent us through the <form>, 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.
                                     <a href="/">Click here to go to the home</a>"#);
        },

        _ => ()
    );

    // We 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#"
                    <p>Hint: in this example all passwords that start with the letter 'b'
                       (lowercase) are valid.</p>
                    <form action="/login" method="POST">
                        <input type="text" name="login" placeholder="Login" />
                        <input type="password" name="password" placeholder="Password" />
                        <button type="submit">Go!</button>
                    </form>
                    <p>Or you can try <a href="/private">going to the private area</a>
                       without logging in, but you will be redirected back here.</p>
                "#)
            },

            _ => {
                // 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.<br />
                              <a href="/private">Click here for the private area</a>
                              <form action="/logout" method="POST">
                              <button>Logout</button></form>"#)
        },

        (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! <a href="/">Go back</a>."#)
        },

        _ => Response::empty_404()
    )
}