#[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() | |
) | |
} |