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}