rouille/
session.rs

1// Copyright (c) 2016 The Rouille developers
2// Licensed under the Apache License, Version 2.0
3// <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6// at your option. All files in the project carrying such
7// notice may not be copied, modified, or distributed except
8// according to those terms.
9
10//! Sessions handling.
11//!
12//! The main feature of this module is the `session` function which handles a session. This
13//! function guarantees that a single unique identifier is assigned to each client. This identifier
14//! is accessible through the parameter passed to the inner closure.
15//!
16//! # Basic example
17//!
18//! Here is a basic example showing how to get a session ID.
19//!
20//! ```
21//! use rouille::Request;
22//! use rouille::Response;
23//! use rouille::session;
24//!
25//! fn handle_request(request: &Request) -> Response {
26//!     session::session(request, "SID", 3600, |session| {
27//!         let id: &str = session.id();
28//!
29//!         // This id is unique to each client.
30//!
31//!         Response::text(format!("Session ID: {}", id))
32//!     })
33//! }
34//! ```
35
36use rand;
37use rand::distributions::Alphanumeric;
38use rand::Rng;
39use std::borrow::Cow;
40use std::sync::atomic::AtomicBool;
41use std::sync::atomic::Ordering;
42
43use input;
44use Request;
45use Response;
46
47pub fn session<'r, F>(request: &'r Request, cookie_name: &str, timeout_s: u64, inner: F) -> Response
48where
49    F: FnOnce(&Session<'r>) -> Response,
50{
51    let mut cookie = input::cookies(request);
52    let cookie = cookie.find(|&(ref k, _)| k == &cookie_name);
53    let cookie = cookie.map(|(_, v)| v);
54
55    let session = if let Some(cookie) = cookie {
56        Session {
57            key_was_retrieved: AtomicBool::new(false),
58            key_was_given: true,
59            key: cookie.into(),
60        }
61    } else {
62        Session {
63            key_was_retrieved: AtomicBool::new(false),
64            key_was_given: false,
65            key: generate_session_id().into(),
66        }
67    };
68
69    let mut response = inner(&session);
70
71    if session.key_was_retrieved.load(Ordering::Relaxed) {
72        // TODO: use `get_mut()`
73        // FIXME: correct interactions with existing headers
74        // TODO: allow setting domain
75        let header_value = format!(
76            "{}={}; Max-Age={}; Path=/; HttpOnly",
77            cookie_name, session.key, timeout_s
78        );
79        response
80            .headers
81            .push(("Set-Cookie".into(), header_value.into()));
82    }
83
84    response
85}
86
87/// Contains the ID of the session.
88pub struct Session<'r> {
89    key_was_retrieved: AtomicBool,
90    key_was_given: bool,
91    key: Cow<'r, str>,
92}
93
94impl<'r> Session<'r> {
95    /// Returns true if the client gave us a session ID.
96    ///
97    /// If this returns false, then we are sure that no data is available.
98    #[inline]
99    pub fn client_has_sid(&self) -> bool {
100        self.key_was_given
101    }
102
103    /// Returns the id of the session.
104    #[inline]
105    pub fn id(&self) -> &str {
106        self.key_was_retrieved.store(true, Ordering::Relaxed);
107        &self.key
108    }
109
110    /*/// Generates a new id. This modifies the value returned by `id()`.
111    // TODO: implement
112    #[inline]
113    pub fn regenerate_id(&self) {
114        unimplemented!()
115    }*/
116}
117
118/// Generates a string suitable for a session ID.
119///
120/// The output string doesn't contain any punctuation or character such as quotes or brackets
121/// that could need to be escaped.
122pub fn generate_session_id() -> String {
123    // 5e+114 possibilities is reasonable.
124    rand::thread_rng()
125        .sample_iter(&Alphanumeric)
126        .map(char::from)
127        .filter(|&c| {
128            ('a'..='z').contains(&c) || ('A'..='Z').contains(&c) || ('0'..='9').contains(&c)
129        })
130        .take(64)
131        .collect::<String>()
132}
133
134#[test]
135fn test_generate_session_id() {
136    assert!(generate_session_id().len() >= 32);
137}