I want to implement sessions in my API (follow up from this question). Actix has some very handy middleware for this which I'd use in a real-life scenario. However, for the sake of learning and because it's really interesting, I want to implement this manually.
I've written a module to handle the sessions.
lazy_static! {
// Session database saved in memory
static ref SESSIONS : Mutex<HashMap<String, Option<u8>>> = Mutex::new(HashMap::new());
}
// Module for handling sessions
pub mod session_handler {
use actix_web::{HttpRequest, HttpResponse};
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;
use sha2::{Sha256, Digest};
use crate::*;
// Generate session_token. Concatenation of sha256(ip_address) and a random string,
// ip_address is included to avoid collission
fn generate_session_token(ip_address: String) -> String {
// Hashing ip_address
let mut sha256 = Sha256::new();
sha256.update(ip_address);
let ip_address_hash : String = format!("{:X}", sha256.finalize());
// Random String
let random_string : String = thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.collect();
format!("{}{}", ip_address_hash, random_string)
}
// Check if a session exists by reading the cookie 'session' in the request-header
pub fn session_exists(req: HttpRequest) -> bool {
let exists = get_session_token(req);
if exists == None {
return false
}
if exists.unwrap() == "" {
return false
}
true
}
// Retrieves the session_token from the request.
// if none cookie is found this Option<String> will be None, otherwise
// it will contain the session_token after unwrapping
pub fn get_session_token(req: HttpRequest) -> Option<String> {
req.headers()
.get("session")
.and_then(|r| r.to_str().map(String::from).ok())
}
// Function to make a HttpResponse with the NEW session_token in the header.
pub fn set_session_token(req: HttpRequest) -> HttpResponse {
let ip_address = req
.connection_info()
.remote_addr()
.unwrap()
.to_string();
let session_token = generate_session_token(ip_address);
let header_string = format!("session={}; Secure; HttpOnly", session_token);
add_session(session_token);
HttpResponse::Ok()
.content_type("plain/text")
.header("Set-Cookie", header_string)
.body("")
}
// Function to save this session in the session_database
pub fn add_session(token: String) {
SESSIONS.lock().unwrap().insert(token, None);
}
// Function to delete a session from the session_database
pub fn delete_session_by_token(token: String) {
SESSIONS.lock().unwrap().remove(&token);
}
// Unimplemented...
pub fn delete_session_by_userid(id: u8) {
unimplemented!()
}
}
The flow of a request would go like this.
Pseudo-code:
// Function executed after Actix' route() function
async fn view_secret_page(req: HttpRequest) -> HttpResponse {
// If the user has session
if session_exists(req) {
// Check if he's logged in
let user_id = get_user_by_session(get_session_token(req));
if user_id == None {
// Not authenticated
return;
} else {
// Show secret page because user is logged in!
// Return HttpOK
}
} else {
// No session is set... So start session for user
set_session_token(req) // returns HttpResponse with cookie in header
}
}
I'd probably implement this in middleware so this is automated by each request and I don't have to do all the checks for each request.
Before continuing the implementation, I'd like to receive some feedback on my module. Is it workable? What can be improved?
Thanks!