blob: 52842a2b60c8a29d10c2d5bddf60e3f6b516d03d [file] [log] [blame]
// Copyright (c) 2016 The Rouille developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Sessions handling.
//!
//! The main feature of this module is the `session` function which handles a session. This
//! function guarantees that a single unique identifier is assigned to each client. This identifier
//! is accessible through the parameter passed to the inner closure.
//!
//! # Basic example
//!
//! Here is a basic example showing how to get a session ID.
//!
//! ```
//! use rouille::Request;
//! use rouille::Response;
//! use rouille::session;
//!
//! fn handle_request(request: &Request) -> Response {
//! session::session(request, "SID", 3600, |session| {
//! let id: &str = session.id();
//!
//! // This id is unique to each client.
//!
//! Response::text(format!("Session ID: {}", id))
//! })
//! }
//! ```
use std::borrow::Cow;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use rand;
use rand::Rng;
use rand::distributions::Alphanumeric;
use Request;
use Response;
use input;
pub fn session<'r, F>(request: &'r Request, cookie_name: &str, timeout_s: u64, inner: F) -> Response
where F: FnOnce(&Session<'r>) -> Response
{
let mut cookie = input::cookies(request).into_iter();
let cookie = cookie.find(|&(ref k, _)| k == &cookie_name);
let cookie = cookie.map(|(_, v)| v);
let session = if let Some(cookie) = cookie {
Session {
key_was_retreived: AtomicBool::new(false),
key_was_given: true,
key: cookie.into(),
}
} else {
Session {
key_was_retreived: AtomicBool::new(false),
key_was_given: false,
key: generate_session_id().into(),
}
};
let mut response = inner(&session);
if session.key_was_retreived.load(Ordering::Relaxed) { // TODO: use `get_mut()`
// FIXME: correct interactions with existing headers
// TODO: allow setting domain
let header_value = format!("{}={}; Max-Age={}; Path=/; HttpOnly",
cookie_name, session.key, timeout_s);
response.headers.push(("Set-Cookie".into(), header_value.into()));
}
response
}
/// Contains the ID of the session.
pub struct Session<'r> {
key_was_retreived: AtomicBool,
key_was_given: bool,
key: Cow<'r, str>,
}
impl<'r> Session<'r> {
/// Returns true if the client gave us a session ID.
///
/// If this returns false, then we are sure that no data is available.
#[inline]
pub fn client_has_sid(&self) -> bool {
self.key_was_given
}
/// Returns the id of the session.
#[inline]
pub fn id(&self) -> &str {
self.key_was_retreived.store(true, Ordering::Relaxed);
&self.key
}
/*/// Generates a new id. This modifies the value returned by `id()`.
// TODO: implement
#[inline]
pub fn regenerate_id(&self) {
unimplemented!()
}*/
}
/// Generates a string suitable for a session ID.
///
/// The output string doesn't contain any punctuation or character such as quotes or brackets
/// that could need to be escaped.
pub fn generate_session_id() -> String {
// 5e+114 possibilities is reasonable.
rand::OsRng::new().expect("Failed to initialize OsRng") // TODO: <- handle that?
.sample_iter(&Alphanumeric)
.filter(|&c| (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9'))
.take(64).collect::<String>()
}