Rack::State – Secure & Modular State Management

Rack::State is middleware that securely stores and manages object state.

Applications use the Manager API to set, get, and delete state of any object that requires persistence in a stateless environment.

The Core Components

Diagram of All the Players

State (middleware)
|
+--> Manager (env[rack.state.KEY]) <-- API (get/set/delete object)
     |
     +--> client-side: cookie (KEY = token)
     |
     +--> server-side: Store (token, object)
                       |
                       +--> Memory
                       +--> File
                       +--> Postgres
                       +--> PreparedPostgres

The KEY and Store adapter may be set for each instance of middleware.

Design & Motivation

Primary goal

State management for multiple objects with independent control of the visibility, security, and storage of each object's state.

But we have Rack::Session

Rack::Session provides state management, but does so using only a single SessionHash, which is statically located in Rack's environment. The drawback is the lack of fine-grained control, efficiency, and multiple instances. This single hash-like object is always available to the entire application whether it is needed or not.

I attempted to modify and bend Rack::Session to meet my design goal, but there seemed to be no clean and easy solution. Rack::State was born…

Use Case

An application has the following areas that require data persistence:

  1. site-wide personalization – real name, theme, etc.

  2. blog activity tracking – articles read, favorites, shares

  3. secure store – TLS for entire store, shopping cart, and checkout

Three instances of Rack::State can be used. The domain, path, expiration, token security (i.e., HttpOnly & Secure cookie flags) and storage backend can be set appropriately for each area.

For example, site-wide personalization may set path to “/” with a long expiration, but the secure store could use “/store” with expiration at end of session and set the HttpOnly and Secure flags as well. Additionally, site-wide personalization and blog activity tracking states could be stored in the database while the secure store state could be saved in files.

Usage Examples

Manage a single state in memory

Here's a simple rackup (config.ru) file that uses the Rack::State middleware with default options.

require 'rack'
require 'rack/state'

use Rack::State

run MyApp

The application can access the State::Manager via the environment. The following code sets up Rack::State to function similarly to Rack::Session.

def session
  env['rack.state.token'].get or env['rack.state.token'].set Hash.new
end

session['username']

Multiple state management with different storage adapters

Below, the flash state will use the default Memory store and store will save state in files under “tmp/state-store” in the project's root. Additionally, if “/store” requires encryption it is wise to secure the token.

use Rack::State, key: 'flash', path: '/', max_age: 60*60
use Rack::State, key: 'store', path: '/store', secure: true,
  store: Rack::State::Store::File.new('tmp/state-store')

Then on the application side you may have some helper methods like this:

def flash
  env['rack.state.flash'].get or env['rack.state.flash'].set Flash.new
end

def store
  env['rack.state.store']
end

store.set SecureStore.new # start shopping session

store.get.cart.add :item7
flash.notice 'Added Item 7'

store.get.transaction.process
flash.notice 'Transaction successful'
store.delete # remove from client/server after checkout

Choose a specific storage adapter depending on the environment

Here we use the Postgres store for production, otherwise use the default Memory store for development and testing.

if ENV['RACK_ENV'] == 'production'
  DB = PG::Connection.new
  use Rack::State, store: Rack::State::Store::Postgres.new(DB)
else
  use Rack::State
end

Keep state of an arbitrary object using helper methods

First setup the middleware key and helpers.

use Rack::State, key: 'myobj'

def my_object
  env['rack.state.myobj'].get
end

def set_my_object(obj)
  obj ? env['rack.state.myobj'].set(obj) : env['rack.state.myobj'].delete
end

Then get state tracking in your app.

set_my_object ObjectA.new  # instance of ObjectA persistently stored
my_object.do_something     # interact with original instance
set_my_object nil          # remove instance from state store
set_my_object ObjectB.new  # do it all again with another object

Install, Test & Contribute

Install the gem:

$ sudo gem install rack-state

Or clone the Mercurial repository:

$ hg clone https://bitbucket.org/pachl/rack-state

State is tested with Christian Neukirchen's awesome test framework, Bacon. Get some bacon and start cooking:

$ sudo gem install bacon

Run the entire test suite from the project's root:

$ bacon -a

To test a specific component, such as a new storage adapter, run:

$ bacon spec/spec_COMPONENT.rb

Submit a patch

After you fix a bug or develop a storage adapter, please submit your code and tests via a Bitbucket pull request.

Or for simple one-off patches, use the following workflow:

$ hg clone https://bitbucket.org/pachl/rack-state
$ cd rack-state
$ # EDIT AND ADD NEW FILES
$ hg add # NEW FILES
$ hg commit -m 'describe your changes'
$ hg export tip > patch.diff
$ mail -s 'rack-state patch' pachl@ecentryx.com < patch.diff
Homepage

ecentryx.com/gems/rack-state

Ruby Gem

rubygems.org/gems/rack-state

Source Code

bitbucket.org/pachl/rack-state/src

Bug Tracker

bitbucket.org/pachl/rack-state/issues

Compatibility

Rack::State was developed and tested on OpenBSD 5.3 using Ruby 1.9.3 and Rack 1.5.

History

  1. 2013-09-05, v0.0.0: Initial design, development and testing

  2. 2013-09-22, v0.0.1: First public release

  3. 2013-09-22, v0.0.2: Update Gem info and documentation

  4. 2014-04-20, v0.0.3: Add homepage and bug tracker URLs

License

(ISC License)

Copyright © 2014, Clint Pachl <pachl@ecentryx.com>

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.