= 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
- Rack::State
- Rack::State::Manager
- Rack::State::Store
== 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:
- site-wide personalization -- real name, theme, etc.
- blog activity tracking -- articles read, favorites, shares
- 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 http://code.pachl.us/rack-state
State is tested with Christian Neukirchen's awesome test framework,
{Bacon}[https://github.com/chneukirchen/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 -Ilib spec/spec_COMPONENT.rb
After you fix a bug or develop a storage adapter, submit your code and tests.
TODO
== Links
Homepage :: http://ecentryx.com/gems/rack-state
Ruby Gem :: https://rubygems.org/gems/rack-state
Source Code :: https://bitbucket.org/pachl/rack-state/src
Bug Tracker :: https://bitbucket.org/pachl/rack-state/issues
== Compatibility
Rack::State was developed and tested on
OpenBSD[http://www.openbsd.org] 5.3
using Ruby[https://www.ruby-lang.org] 1.9.3
and Rack[https://github.com/rack/rack] 1.5.
== History
- 2013-09-05, v0.0.0: Initial design, development and testing
- 2013-09-22, v0.0.1: First public release
- 2013-09-22, v0.0.2: Update Gem info and documentation
- 2014-04-20, v0.0.3: Add homepage and bug tracker URLs
== License
({ISC License}[http://opensource.org/licenses/ISC])
Copyright (c) 2013, 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.