Functions to implement sessions

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!

Using a static to implement middleware is definitely not recommended, since all instances would share the same session list. It'd also be good to use a concurrent map implementation like dashmap to allow multiple users to connect to the server at once (which is kinda the point :stuck_out_tongue:)

At a finer level, I think your token generation could be improved. Using uuid would keep it simple (while avoiding allocating each token too), and you can prevent collisions by making sure it isn't already in the session list. You can probably tune the hasher to be very quick since you're generating the keys, and they'll only be a few bytes long.

Then some style stuff:

fn session_exists(req: &HttpRequest) -> bool {
    get_session_token(req).map_or(false, |s| s != "")
}
fn get_session_token(req: &HttpRequest) -> Option<&str> {
    req.headers().get("session").and_then(|r| r.to_str())
    // and maybe .parse().ok() if you're using Uuids
}
1 Like

Given that most of the worlds browsers are on on local networks behind NAT it is possible that requests come from the same public IP address.

Random numbers can of course happen to be the same on on occasion.

1 Like

While obviously true, it's exponentially less likely with more digits, so not really a practical problem. As the saying goes,

The probability that your entire development team is killed by wolves in the same night, at precisely the same point in time where a meteor lands on your data center, is much higher than the collision probability of UUIDs
~ https://stackoverflow.com/questions/46779530/how-to-ensure-that-generated-id-does-not-exist-in-database#comment80517638_46779530

And it looks like the code above is using even more random bits than the ~122 in a UUID. Though it should really just use a v4 UUID directly, rather than reinventing one poorly...

2 Likes

There are many ways to get collisions in UUIDs other than just bad luck. A bad guy could introduce one on purpose. A hot spare might necessarily use the same UUIDs as the primary. There can be an unknown flaw in the random number generator.

There's also what happened at the Lawrence Berkeley Lab Supercomputer Center. They had two 128K node systems that they assembled into a single 256K node system, but it wouldn't boot up. After a couple of weeks' effort, they discovered that two NICs had the same MAC address.

All that being said, I'm using UUIDs in my project with no provision for dealing with collisions.

I think that is what I was trying to say.

I'll switch my token_generation to UUID. And how would I implement the dashmap? Can I also make that kind of global so that all functions can easily access it?

The following compiles.

lazy_static! {
    static ref SESSIONS : Mutex<DashMap<String, Option<u8>>> = Mutex::new(DashMap::new());
}

Is this how to implement a Dashmap? And Dashmap is like Hashmap but than 'shared' with all users?

A dashmap (concurrent hashmap) allows mutation of the inner elements with only a &self (thus enabling multiple concurrent writes through the use of reader). It accmplishes this through the use of atomic locking mechanisms. You can (very roughly) emulate a concurrent hashmap by wrapping every T in a HashMap with a RwLock<T> or Mutex<T>, however, there are lock-free versions (e.g., evmap) that fundamentally use CAS operations.

Also, since you need concurrent read access to the DashMap, I wouldn't wrap SESSIONS with a Mutex; instead use a RwLock and call read() on it to access the dashmap (also, you might be able to get away with no outer wrapper; depends on whether or not DashMap implements the right traits). This will allow you to hold multiple read-locks which thereby enable you to mutate the inner elements of the DashMap

1 Like

Okay thanks all. This is what I have now:

lazy_static! {
    static ref SESSIONS : RwLock<DashMap<String, Option<u8>>> = RwLock::new(DashMap::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 uuid::Uuid;
    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() -> String {
        Uuid::new_v4().to_string()
    }


    // Check if a session exists by reading the cookie 'session' in the request-header
    pub fn session_exists(req: HttpRequest) -> bool {
        get_session_token(req).map_or(false, |s| s != "")
    }

    // 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> {
        Some(req.headers().get("session").and_then(|r| r.to_str().ok()).unwrap().to_string())

    }

    // 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();
        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("")
    }

    // Save this session in the session_database
    pub fn add_session(token: String) {
        SESSIONS.read().unwrap().insert(token, None);
    }

    // Delete a session from the session_database
    pub fn delete_session_by_token(token: String) {
        SESSIONS.read().unwrap().remove(&token);
    }
    // Unimplemented...
    pub fn delete_session_by_userid(id: u8) {
        unimplemented!()
    }

    // A user is linked to a session after authenticating himself. This Function
    // can retrieve which user is connected to which session.
    pub fn get_user_by_session(token: String) -> Option<u8> {
        *SESSIONS.read().unwrap().get(&token).unwrap()
    }

    pub fn set_user_by_session(token: String, user_id: u8) {
        if let Some(mut value) = SESSIONS.read().unwrap().get_mut(&token) {
            *value = Some(user_id);
        }
    }

}

If this is okay I'll try to actually implement it.

1 Like