Custom session storage with newLISP Web

The Web module’s default session storage engine uses serialized contexts in the /tmp directory. The advantage of this method is that, apart from the directory (which is customizable), it is reasonably platform independent and has low overhead. The disadvantages of this are numerous. Files are stored unencrypted, so anyone with access to the server may view them. It is therefor advantageous to design a custom storage module for sessions.

Storing sessions in a database is a good solution. Backups are easier; persistence and pruning orphaned sessions are simpler. Security is available through the database server’s security. We will use MySQL, since it is the best supported in newLISP.

Setting up the database

First, we need a table in which to store session data. Using a MySQL TIMESTAMP column will provide us with an automatically updating time stamp on the last modification of the session, which we will use below to find old sessions to remove.

CREATE TABLE sessions (
        sid VARCHAR(255) PRIMARY KEY,
        DATA TEXT NOT NULL,
        ts TIMESTAMP
)

Designing the storage module

Several functions need to be defined to handle storing data. First, we must ensure that the MySQL module is loaded and initialized. We will do this in a separate module that we will call DBSessions.

(context 'DBSessions)
 
(unless (sym "MYSQL" 'MySQL nil) ; do not reinitialize if already loaded
  (module "mysql5.lsp") ; with newlisp 10.1, this will change to "mysql.lsp"
  (MySQL:init)
  (MySQL:connect "127.0.0.1" "user" "secret" "somedb"))
 
(context 'MAIN)

Next, we define a function to create or resume a session from the database. Sessions are stored in memory as contexts, making our job easier. Our function will query MySQL to see if the session id exists, and either create a new session and insert it or retrieve an existing session and load it. The function Web:session-id gives us the current session id from the session cookie or creates a new one and sends the client a cookie to store it. Web:session-context returns a symbol pointed to the context storing the current session.

(define (open-session)
  (when (MySQL:query (format "SELECT data FROM sessions WHERE sid = '%s'" (MySQL:escape (Web:session-id))))
    (let ((data (MySQL:fetch-row)))
      (if data
        ;; Evaluate the serialized context into the current session context
        (eval-string (first data) (Web:session-context))
        ;; Create a new session and save it
        (MySQL:query (format "INSERT INTO sessions (sid, data) VALUES ('%s', '%s')"
          (MySQL:escape (Web:session-id))
          (MySQL:escape (source (Web:session-context)))))))))

Next, we need a function to write any changes to the session to storage. Since the session is stored as a context, this is quite simple.

(define (close-session)
  (MySQL:query (format "UPDATE sessions SET data = '%s' WHERE sid = '%s'"
    (MySQL:escape (source (Web:session-context)))
    (MySQL:escape (Web:session-id)))))

We also need a function to delete entire sessions when necessary. This is also simple.

(define (delete-session)
  (MySQL:query (format "DELETE FROM sessions WHERE sid = '%s'"
    (MySQL:escape (Web:session-id)))))

Finally, a function to cull old sessions from the database. For this, we use the variable Web:SESSION_MAX_AGE to determine which sessions have expired.

(define (clean-sessions)
  (MySQL:query (format "DELETE FROM sessions WHERE NOW() >= DATE_ADD(ts, INTERVAL %d SECOND)"
    Web:SESSION_MAX_AGE)))

Registering the handler in Web

Last, the handler functions need to be registered in the Web module.

(Web:define-session-handlers
  open-session close-session delete-session clean-sessions)

Putting it all together

(unless (sym "MYSQL" 'MySQL nil) ; do not reinitialize if already loaded
  (module "mysql5.lsp") ; with newlisp 10.1, this will change to "mysql.lsp"
  (MySQL:init)
  (MySQL:connect "127.0.0.1" "user" "secret" "somedb"))
 
(context 'DBSessions)
 
(define (open-session)
  (when (MySQL:query (format "SELECT data FROM sessions WHERE sid = '%s'" (MySQL:escape (Web:session-id))))
    (let ((data (MySQL:fetch-row)))
      (if data
        ;; Evaluate the serialized context into the current session context
        (eval-string (first data) (Web:session-context))
        ;; Create a new session and save it
        (MySQL:query (format "INSERT INTO sessions (sid, data) VALUES ('%s', '%s')"
          (MySQL:escape (Web:session-id))
          (MySQL:escape (source (Web:session-context)))))))))
 
(define (close-session)
  (MySQL:query (format "UPDATE sessions SET data = '%s' WHERE sid = '%s'"
    (MySQL:escape (source (Web:session-context)))
    (MySQL:escape (Web:session-id)))))
 
(define (delete-session)
  (MySQL:query (format "DELETE FROM sessions WHERE sid = '%s'"
    (MySQL:escape (Web:session-id)))))
 
(define (clean-sessions)
  (MySQL:query (format "DELETE FROM sessions WHERE NOW() >= DATE_ADD(ts, INTERVAL %d SECOND)"
    Web:SESSION_MAX_AGE)))
 
(Web:define-session-handlers
  open-session close-session delete-session clean-sessions)
 
(context 'MAIN)
Leave a comment | Trackback
May 29th, 2009 | Posted in Programming
Tags: , ,