blob: 16785c2c4f41198f6e4a2372316320a537c27387 [file] [log] [blame] [edit]
#[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()
)
}