Commits

Michael Granger  committed dc4b699

Adding a websocket service base class

  • Participants
  • Parent commits 48fb08c

Comments (0)

Files changed (28)

 Make a MacOS X tool like Pow! that makes it easy to run Strelka apps with a
 minimum of setup.
 
+
+== WebSocketServer
+
+Planned features:
+
+* DSL plugin for handling various kinds of frames, ala the App routing plugin
+* DSL plugin derived from the frame-based routing plugin that adds routing logic based on a
+  JSON data structure's contents
+* Automatic de-fragmenting of frames, with a plugin that allows customization of fragment-handling.
+* Heartbeat plugin that automatically pings connected clients, and disconnects them if they haven't
+  been seen in a while.
+* Plugin class to facilitate extensions? Not sure how this would work, but it could use
+  the 'deflate' extension as the test case.
+
+
+

File MILESTONES.rdoc

 
 === Documentation
 
-[ ] Ensure the README is up to date
+[√] Ensure the README is up to date
 [ ] Update the IDEAS doc
 [ ] Extract the rest of the tutorial part of the manual out into RDoc for the app classes
 [ ] Lay out the framework for the 'cookbook' section of the manual
 # vim: set nosta noet ts=4 sw=4:
 
 require 'strelka'
+require 'strelka/discovery'
+
 require 'trollop'
 require 'highline'
 require 'loggability'
 		# Set the datadir override if it's given
 		if self.options.datadir
 			self.log.debug "Using data dir option: %s" % [ self.options.datadir ]
-			Strelka::App.local_data_dirs = Pathname( self.options.datadir )
+			Strelka::Discovery.local_data_dirs = Pathname( self.options.datadir )
 		end
 
 		# Include a 'lib' directory if there is one
 
 		self.load_additional_requires
 
-		paths = Strelka::App.discover_paths
+		paths = Strelka::Discovery.discover_paths
 		if paths.empty?
 			message "None found."
 		else
 	def start_command( *args )
 		appname = args.pop
 		gemname = args.pop
-		path, gemname = Strelka::App.find( appname, gemname )
+		path, gemname = Strelka::Discovery.find( appname, gemname )
 
 		header "Starting the %s app%s" % [
 			appname,
 			gemname == '' ? '' : " from the #{gemname} gem"
 		]
 
-		apps = Strelka::App.load( path )
+		apps = Strelka::Discovery.load( path )
 		Strelka.load_config( self.options.config ) if self.options.config
 		self.log.debug "  loaded: %p" % [ apps ]
 
 		discovery_name = gemname || ''
 
 		header "Dumping config for %s" % [ gemname || 'local apps' ]
-		discovered_apps = Strelka::App.discover_paths
+		discovered_apps = Strelka::Discovery.discover_paths
 
 		raise ArgumentError, "No apps discovered" unless discovered_apps.key?( discovery_name )
 
 		discovered_apps[ discovery_name ].each do |apppath|
 			message "  loading %s (%s)" % [ apppath, apppath.basename('.rb') ]
-			Strelka::App.load( apppath )
+			Strelka::Discovery.load( apppath )
 		end
 
 		self.load_additional_requires

File examples/Procfile

 auth2: ../bin/strelka -D . -l warn -c config.yml start auth-demo2
 sessions: ../bin/strelka -D . -l debug -c config.yml start sessions-demo
 upload: ../bin/strelka -D . -l debug -c config.yml start upload-demo
-# ws: ../bin/strelka -l info -c config.yml start ws-echo
+ws: ../bin/strelka -l info -c config.yml start ws-echo
 

File examples/apps/hello-world

 
 
 # Run the app
+Encoding.default_internal = Encoding::UTF_8
 HelloWorldApp.run if __FILE__ == $0

File examples/apps/ws-chat

+#!/usr/bin/env ruby
+# encoding: utf-8
+
+require 'strelka/websocketserver'
+
+
+# An example of a Strelka WebSocketServer that echoes back whatever (non-control) frames you send
+# it.
+class WebSocketChatServer < Strelka::WebSocketServer
+	include Mongrel2::WebSocket::Constants
+
+
+	### Set up the user registry.
+	def initialize( * )
+		super
+		@users = {}
+	end
+
+
+	#
+	# Heartbeat plugin
+	#
+	plugin :heartbeat
+
+	heartbeat_rate 5.0
+	idle_timeout 15.0
+
+
+	# Make a new user slot for sockets when they start up.
+	on_handshake do |frame|
+		super
+		@users[ frame.socket_id ] = nil
+	end
+
+
+	# When a text frame comes in (or is assembled from :continuation frames), parse it as JSON
+	# and decide what to do based on its contents.
+	on_text do |frame|
+		
+	end
+
+
+
+	# Handle close frames
+	on_close do |frame|
+
+		username = self.users.delete( frame.socket_id )
+		self.broadcast_notice( "#{username} disconnected." ) if username
+
+		# There will still be a connection slot if this close originated with
+		# the client. In that case, reply with the ACK CLOSE frame
+		self.conn.reply( frame.response(:close) ) if
+			self.connections.delete( [frame.sender_id, frame.conn_id] )
+
+		self.conn.reply_close( frame )
+		return nil
+	end
+
+
+end # class RequestDumper
+
+Loggability.level = $DEBUG||$VERBOSE ? :debug : :info
+Loggability.format_as( :color ) if $stdin.tty?
+
+# Point to the config database, which will cause the handler to use
+# its ID to look up its own socket info.
+Mongrel2::Config.configure( :configdb => 'examples.sqlite' )
+WebSocketEchoServer.run( 'ws-echo' )
+

File examples/apps/ws-echo

+#!/usr/bin/env ruby
+# encoding: utf-8
+
+require 'strelka/websocketserver'
+
+
+# An example of a Strelka WebSocketServer that echoes back whatever (non-control) frames you send
+# it.
+class WebSocketEchoServer < Strelka::WebSocketServer
+
+	# Application ID
+	ID = 'ws-echo'
+
+	#
+	# Heartbeat plugin
+	#
+	# plugin :heartbeat
+	#
+	# heartbeat_rate 5.0
+	# idle_timeout 15.0
+
+
+	#
+	# Routing
+	#
+
+	plugin :routing
+
+	# Handle TEXT, BINARY, and CONTINUATION frames by replying with an echo of the
+	# same data. Fragmented frames get echoed back as-is without any reassembly.
+	on_text do |frame|
+		self.log.info "Echoing data frame: %p" % [ frame ]
+
+		# Make the response frame
+		response = frame.response
+		response.fin = frame.fin?
+		IO.copy_stream( frame.payload, response.payload )
+
+		return response
+	end
+	alias_method :handle_binary_frame, :on_text_frame
+	alias_method :handle_continuation_frame, :on_text_frame
+
+
+	# Handle close frames
+	on_close do |frame|
+
+		# There will still be a connection slot if this close originated with
+		# the client. In that case, reply with the ACK CLOSE frame
+		self.conn.reply( frame.response(:close) ) if
+			self.connections.delete( [frame.sender_id, frame.conn_id] )
+
+		self.conn.reply_close( frame )
+		return nil
+	end
+
+
+end # class RequestDumper
+
+Encoding.default_internal = Encoding::UTF_8
+WebSocketEchoServer.run if __FILE__ == $0

File lib/strelka.rb

 	require 'strelka/app'
 	require 'strelka/httprequest'
 	require 'strelka/httpresponse'
+	require 'strelka/discovery'
 
 
 	### Get the library version. If +include_buildnum+ is true, the version string will
 	### named +gemname+. Returns the first matching class, or raises an exception if no
 	### app class was found.
 	def self::App( appname, gemname=nil )
-		path, _ = Strelka::App.find( appname, gemname )
+		path, _ = Strelka::Discovery.find( appname, gemname )
 		raise LoadError, "Can't find the %s app." % [ appname ] unless path
 
-		apps = Strelka::App.load( path ) or
+		apps = Strelka::Discovery.load( path ) or
 			raise ScriptError "Loading %s didn't define a Strelka::App class." % [ path ]
 
 		return apps.first

File lib/strelka/app.rb

 require 'strelka' unless defined?( Strelka )
 require 'strelka/mixins'
 require 'strelka/plugins'
+require 'strelka/discovery'
 
 
 # The Strelka HTTP application base class.
 class Strelka::App < Mongrel2::Handler
 	extend Loggability,
 	       Configurability,
+		   Strelka::Discovery,
 	       Strelka::MethodUtilities,
 	       Strelka::PluginLoader
 	include Strelka::Constants,
 	### is loaded.
 	def self::configure( config=nil )
 		config = self.defaults.merge( config || {} )
-
 		self.devmode = config[:devmode] || $DEBUG
-		self.app_glob_pattern = config[:app_glob_pattern]
-		self.local_data_dirs  = config[:local_data_dirs]
-
 		self.log.info "Enabled developer mode." if self.devmode?
 	end
 
 
-	### Inheritance callback -- add subclasses to @subclasses so .load can figure out which
-	### classes correspond to which files.
-	def self::inherited( subclass )
-		super
-		self.log.debug "Adding %p to the subclasses hash." % [ subclass ]
-		Strelka::App.subclasses[ @loading_file ] << subclass #if self == Strelka::App
-	end
-
-
-
 	### Overridden from Mongrel2::Handler -- use the value returned from .default_appid if
 	### one is not specified, and automatically install the config DB if it hasn't been
 	### already.
 	end
 
 
-	### Return a Hash of glob patterns for matching data directories for the latest
-	### versions of all installed gems which have a dependency on Strelka, keyed
-	### by gem name.
-	def self::discover_data_dirs
-		datadirs = {
-			'' => self.local_data_dirs
-		}
-
-		# Find all the gems that depend on Strelka
-		gems = Gem::Specification.find_all do |gemspec|
-			gemspec.dependencies.find {|dep| dep.name == 'strelka'}
-		end
-
-		self.log.debug "Found %d gems with a Strelka dependency" % [ gems.length ]
-
-		# Find all the files under those gems' data directories that match the application
-		# pattern
-		gems.sort.reverse.each do |gemspec|
-			# Only look at the latest version of the gem
-			next if datadirs.key?( gemspec.name )
-			datadirs[ gemspec.name ] = File.join( gemspec.full_gem_path, "data", gemspec.name )
-		end
-
-		self.log.debug "  returning data directories: %p" % [ datadirs ]
-		return datadirs
-	end
-
-
-	### Return a Hash of Strelka app files as Pathname objects from installed gems,
-	### keyed by gemspec name .
-	def self::discover_paths
-		appfiles = {}
-
-		self.discover_data_dirs.each do |gemname, dir|
-			pattern = File.join( dir, self.app_glob_pattern )
-			appfiles[ gemname ] = Pathname.glob( pattern )
-		end
-
-		return appfiles
-	end
-
-
-	### Return an Array of Strelka::App classes loaded from the installed Strelka gems.
-	def self::discover
-		discovered_apps = []
-		app_paths = self.discover_paths
-
-		self.log.debug "Loading apps from %d discovered paths" % [ app_paths.length ]
-		app_paths.each do |gemname, paths|
-			self.log.debug "  loading gem %s" % [ gemname ]
-			gem( gemname ) unless gemname == ''
-
-			self.log.debug "  loading apps from %s: %d handlers" % [ gemname, paths.length ]
-			paths.each do |path|
-				classes = begin
-					Strelka::App.load( path )
-				rescue StandardError, ScriptError => err
-					self.log.error "%p while loading Strelka apps from %s: %s" %
-						[ err.class, path, err.message ]
-					self.log.debug "Backtrace: %s" % [ err.backtrace.join("\n\t") ]
-					[]
-				end
-				self.log.debug "  loaded app classes: %p" % [ classes ]
-
-				discovered_apps += classes
-			end
-		end
-
-		return discovered_apps
-	end
-
-
-	### Find the first app with the given +appname+ and return the path to its file and the name of
-	### the gem it's from. If the optional +gemname+ is given, only consider apps from that gem.
-	### Raises a RuntimeError if no app with the given +appname+ was found.
-	def self::find( appname, gemname=nil )
-		discovered_apps = self.discover_paths
-
-		path = nil
-		if gemname
-			discovered_apps[ gemname ].each do |apppath|
-				self.log.debug "    %s (%s)" % [ apppath, apppath.basename('.rb') ]
-				if apppath.basename('.rb').to_s == appname
-					path = apppath
-					break
-				end
-			end
-		else
-			self.log.debug "No gem name; searching them all:"
-			discovered_apps.each do |disc_gemname, paths|
-				self.log.debug "  %s: %d paths" % [ disc_gemname, paths.length ]
-				path = paths.find do |apppath|
-					self.log.debug "    %s (%s)" % [ apppath, apppath.basename('.rb') ]
-					self.log.debug "    %p vs. %p" % [ apppath.basename('.rb').to_s, appname ]
-					apppath.basename('.rb').to_s == appname
-				end or next
-				gemname = disc_gemname
-				break
-			end
-		end
-
-		unless path
-			msg = "Couldn't find an app named '#{appname}'"
-			msg << " in the #{gemname} gem" if gemname
-			raise( msg )
-		end
-		self.log.debug "  found: %s" % [ path ]
-
-		return path, gemname
-	end
-
-
-	### Load the specified +file+, and return any Strelka::App subclasses that are loaded
-	### as a result.
-	def self::load( file )
-		self.log.debug "Loading application/s from %p" % [ file ]
-		@loading_file = Pathname( file ).expand_path
-		self.subclasses.delete( @loading_file )
-		Kernel.load( @loading_file.to_s )
-		new_subclasses = self.subclasses[ @loading_file ]
-		self.log.debug "  loaded %d new app class/es" % [ new_subclasses.size ]
-
-		return new_subclasses
-	ensure
-		@loading_file = nil
-	end
-
-
 	#
 	# :section: Application declarative methods
 	#

File lib/strelka/app/templating.rb

 	### Return an Array of Pathnames to all directories named 'templates' under the
 	### data dirctories of loaded gems which have a dependency on Strelka.
 	def self::discover_template_dirs
-		directories = Strelka::App.discover_data_dirs.values.flatten
+		directories = Strelka::Discovery.discover_data_dirs.values.flatten
 
 		self.log.debug "Discovered data directories: %p" % [ directories ]
 

File lib/strelka/behavior/plugin.rb

 
 
 # This is a shared behavior for specs which different Strelka::App
-# plugins share in common. If you're creating a Strelka::App plugin,
+# plugins share in common. If you're creating A Strelka Plugin,
 # you can test its conformity to the expectations placed on them by
 # adding this to your spec:
 #
 #
 #    describe YourPlugin do
 #
-#      it_should_behave_like "A Strelka::App Plugin"
+#      it_should_behave_like "A Strelka Plugin"
 #
 #    end
 
-shared_examples_for "A Strelka::App Plugin" do
+shared_examples_for "A Strelka Plugin" do
 
 	let( :plugin ) do
 		described_class

File lib/strelka/discovery.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+require 'rubygems'
+require 'strelka' unless defined?( Strelka )
+
+
+# The Strelka application-discovery system.
+module Strelka::Discovery
+	extend Loggability,
+	       Configurability,
+	       Strelka::MethodUtilities
+
+
+	# Loggability API -- log to the Strelka logger
+	log_to :strelka
+
+	# Configurability API -- use the 'discovery' section of the config
+	config_key :discovery
+
+
+	# Default config
+	CONFIG_DEFAULTS = {
+		app_glob_pattern: '{apps,handlers}/**/*',
+		local_data_dirs:  'data/*',
+	}.freeze
+
+
+	##
+	# The Hash of Strelka::App subclasses, keyed by the Pathname of the file they were
+	# loaded from, or +nil+ if they weren't loaded via ::load.
+	singleton_attr_reader :subclasses
+
+	##
+	# The glob(3) pattern for matching Apps during discovery
+	singleton_attr_accessor :app_glob_pattern
+
+	##
+	# The glob(3) pattern for matching local data directories during discovery. Local
+	# data directories are evaluated relative to the CWD.
+	singleton_attr_accessor :local_data_dirs
+
+	##
+	# The name of the file that's currently being loaded (if any)
+	singleton_attr_reader :loading_file
+
+
+	# Module instance variables
+	@subclasses       = Hash.new {|h,k| h[k] = [] }
+	@loading_file     = nil
+	@app_glob_pattern = CONFIG_DEFAULTS[:app_glob_pattern]
+	@local_data_dirs  = CONFIG_DEFAULTS[:local_data_dirs]
+
+
+	### Configure the App. Override this if you wish to add additional configuration
+	### to the 'app' section of the config that will be passed to you when the config
+	### is loaded.
+	def self::configure( config=nil )
+		config = self.defaults.merge( config || {} )
+
+		self.app_glob_pattern = config[:app_glob_pattern]
+		self.local_data_dirs  = config[:local_data_dirs]
+	end
+
+
+	### Return a Hash of glob patterns for matching data directories for the latest
+	### versions of all installed gems which have a dependency on Strelka, keyed
+	### by gem name.
+	def self::discover_data_dirs
+		datadirs = {
+			'' => self.local_data_dirs
+		}
+
+		# Find all the gems that depend on Strelka
+		gems = Gem::Specification.find_all do |gemspec|
+			gemspec.dependencies.find {|dep| dep.name == 'strelka'}
+		end
+
+		self.log.debug "Found %d gems with a Strelka dependency" % [ gems.length ]
+
+		# Find all the files under those gems' data directories that match the application
+		# pattern
+		gems.sort.reverse.each do |gemspec|
+			# Only look at the latest version of the gem
+			next if datadirs.key?( gemspec.name )
+			datadirs[ gemspec.name ] = File.join( gemspec.full_gem_path, "data", gemspec.name )
+		end
+
+		self.log.debug "  returning data directories: %p" % [ datadirs ]
+		return datadirs
+	end
+
+
+	### Return a Hash of Strelka app files as Pathname objects from installed gems,
+	### keyed by gemspec name .
+	def self::discover_paths
+		appfiles = {}
+
+		self.discover_data_dirs.each do |gemname, dir|
+			pattern = File.join( dir, self.app_glob_pattern )
+			appfiles[ gemname ] = Pathname.glob( pattern )
+		end
+
+		return appfiles
+	end
+
+
+	### Return an Array of Strelka::App classes loaded from the installed Strelka gems.
+	def self::discover
+		discovered_apps = []
+		app_paths = self.discover_paths
+
+		self.log.debug "Loading apps from %d discovered paths" % [ app_paths.length ]
+		app_paths.each do |gemname, paths|
+			self.log.debug "  loading gem %s" % [ gemname ]
+			gem( gemname ) unless gemname == ''
+
+			self.log.debug "  loading apps from %s: %d handlers" % [ gemname, paths.length ]
+			paths.each do |path|
+				classes = begin
+					self.load( path )
+				rescue StandardError, ScriptError => err
+					self.log.error "%p while loading Strelka apps from %s: %s" %
+						[ err.class, path, err.message ]
+					self.log.debug "Backtrace: %s" % [ err.backtrace.join("\n\t") ]
+					[]
+				end
+				self.log.debug "  loaded app classes: %p" % [ classes ]
+
+				discovered_apps += classes
+			end
+		end
+
+		return discovered_apps
+	end
+
+
+	### Find the first app with the given +appname+ and return the path to its file and the name of
+	### the gem it's from. If the optional +gemname+ is given, only consider apps from that gem.
+	### Raises a RuntimeError if no app with the given +appname+ was found.
+	def self::find( appname, gemname=nil )
+		discovered_apps = self.discover_paths
+
+		path = nil
+		if gemname
+			discovered_apps[ gemname ].each do |apppath|
+				self.log.debug "    %s (%s)" % [ apppath, apppath.basename('.rb') ]
+				if apppath.basename('.rb').to_s == appname
+					path = apppath
+					break
+				end
+			end
+		else
+			self.log.debug "No gem name; searching them all:"
+			discovered_apps.each do |disc_gemname, paths|
+				self.log.debug "  %s: %d paths" % [ disc_gemname, paths.length ]
+				path = paths.find do |apppath|
+					self.log.debug "    %s (%s)" % [ apppath, apppath.basename('.rb') ]
+					self.log.debug "    %p vs. %p" % [ apppath.basename('.rb').to_s, appname ]
+					apppath.basename('.rb').to_s == appname
+				end or next
+				gemname = disc_gemname
+				break
+			end
+		end
+
+		unless path
+			msg = "Couldn't find an app named '#{appname}'"
+			msg << " in the #{gemname} gem" if gemname
+			raise( msg )
+		end
+		self.log.debug "  found: %s" % [ path ]
+
+		return path, gemname
+	end
+
+
+	### Load the specified +file+, and return any Strelka::App subclasses that are loaded
+	### as a result.
+	def self::load( file )
+		self.log.debug "Loading application/s from %p" % [ file ]
+		@loading_file = Pathname( file ).expand_path
+		self.subclasses.delete( @loading_file )
+		Kernel.load( @loading_file.to_s )
+		new_subclasses = self.subclasses[ @loading_file ]
+		self.log.debug "  loaded %d new app class/es" % [ new_subclasses.size ]
+
+		return new_subclasses
+	ensure
+		@loading_file = nil
+	end
+
+
+	### Register the given +subclass+ as having inherited a class that has been extended
+	### with Discovery.
+	def self::add_inherited_class( subclass )
+		self.log.debug "Registering discovered subclass %p" % [ subclass ]
+		self.subclasses[ self.loading_file ] << subclass
+	end
+
+
+	### Inheritance callback -- register the subclass with its parent for discovery.
+	def inherited( subclass )
+		super
+		Strelka::Discovery.log.info "%p inherited by discoverable class %p" % [ self, subclass ]
+		Strelka::Discovery.add_inherited_class( subclass )
+	end
+
+end # module Strelka::Discovery
+

File lib/strelka/websocketserver.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+require 'mongrel2/handler'
+require 'mongrel2/websocket'
+
+require 'strelka' unless defined?( Strelka )
+require 'strelka/mixins'
+require 'strelka/plugins'
+require 'strelka/discovery'
+
+
+# WebSocket (RFC 6455) Server base class.
+#
+#   class ChatServer < Strelka::WebSocketServer
+#
+#       # Set up a Hash for participating users
+#       def initialize( * )
+#           super
+#           @users = {}
+#       end
+#
+#       # Disconnect clients that don't answer a ping
+#       plugin :heartbeat
+#       heartbeat_rate 5.0
+#       idle_timeout 15.0
+#
+#       # When a websocket is set up, add a new user to the table, but without a nick.
+#       on_handshake do |frame|
+#           @users[ frame.socket_id ] = nil
+#           return frame.response # accept the connection
+#       end
+#
+#       # Handle incoming commands, which should be text frames
+#       on_text do |frame|
+#           senderid = frame.socket_id
+#           data = frame.payload.read
+#
+#           # If the input starts with '/', it's a command (e.g., /quit, /nick, etc.)
+#           output = nil
+#           if data.start_with?( '/' )
+#               output = self.command( senderid, data[1..-1] )
+#           else
+#               output = self.say( senderid, data )
+#           end
+#
+#           response = frame.response
+#           response.puts( output )
+#           return response
+#       end
+#
+#   end # class ChatServer
+#
+class Strelka::WebSocketServer < Mongrel2::Handler
+	extend Strelka::MethodUtilities,
+	       Strelka::PluginLoader,
+		   Strelka::Discovery
+
+
+	# Loggability API -- log to the Strelka logger
+	log_to :strelka
+
+
+	### Handle a WebSocket frame in +request+. If not overridden, WebSocket connections are
+	### closed with a policy error status.
+	def handle_websocket( frame )
+		response = nil
+
+		# Dispatch the frame
+		response = catch( :close_websocket ) do
+			self.log.debug "Incoming WEBSOCKET frame (%p):%s" % [ frame, frame.headers.path ]
+			self.handle_frame( frame )
+		end
+
+		return response
+	end
+
+
+	### Handle a WebSocket handshake HTTP +request+.
+	def handle_websocket_handshake( handshake )
+		self.log.warn "Incoming WEBSOCKET_HANDSHAKE request (%p)" % [ request.headers.path ]
+		return handshake.response( handshake.protocols.first )
+	end
+
+
+	### Handle a disconnect notice from Mongrel2 via the given +request+. Its return value
+	### is ignored.
+	def handle_disconnect( request )
+		self.log.info "Unhandled disconnect notice."
+		return nil
+	end
+
+
+	#########
+	protected
+	#########
+
+	### Default frame handler.
+	def handle_frame( frame )
+		if frame.control?
+			self.handle_control_frame( frame )
+		else
+			self.handle_content_frame( frame )
+		end
+	end
+
+
+	### Throw a :close_websocket frame that will close the current connection.
+	def close_with( frame, reason )
+		self.log.debug "Closing the connection: %p" % [ reason ]
+
+		# Make a CLOSE frame
+		frame = frame.response( :close )
+		frame.set_status( reason )
+
+		throw :close_websocket, frame
+	end
+
+
+	### Handle an incoming control frame.
+	def handle_control_frame( frame )
+		self.log.debug "Handling control frame: %p" % [ frame ]
+
+		case frame.opcode
+		when :ping
+			return frame.response
+		when :pong
+			return nil
+		when :close
+			self.conn.reply_close( frame )
+			return nil
+		else
+			self.close_with( frame, CLOSE_BAD_DATA_TYPE )
+		end
+	end
+
+
+	### Handle an incoming content frame.
+	def handle_content_frame( frame )
+		self.log.warn "Unhandled frame type %p" % [ frame.opcode ]
+		self.close_with( frame, CLOSE_BAD_DATA_TYPE )
+	end
+
+
+end # class Strelka::WebSocketServer
+

File lib/strelka/websocketserver/routing.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+require 'strelka' unless defined?( Strelka )
+require 'strelka/websocketserver' unless defined?( Strelka::WebSocketServer )
+require 'strelka/plugin' unless defined?( Strelka::Plugin )
+
+# Frame routing logic for Strelka WebSocketServers.
+#
+# For a protocol that defines its own opcodes:
+#
+#    class ChatServer
+#        plugin :routing
+#
+#        opcodes :nick => 7,
+#                :emote => 8
+#
+#        on_text do |frame|
+#            # ...
+#        end
+#
+#        on_nick do |frame|
+#            self.set_nick( frame.socket_id, frame.payload.read )
+#        end
+#
+#
+module Strelka::WebSocketServer::Routing
+	extend Loggability,
+	       Strelka::Plugin
+	include Strelka::Constants,
+	        Mongrel2::WebSocket::Constants
+
+
+	# Loggability API -- set up logging under the 'strelka' log host
+	log_to :strelka
+
+	# Plugins API -- set up load order
+	# run_after :templating, :filters, :parameters
+
+
+	# Class methods to add to classes with routing.
+	module ClassMethods # :nodoc:
+
+		# The list of routes to pass to the Router when the application is created
+		attr_reader :op_callbacks
+		@op_callbacks = {}
+
+		# The Hash of opcodes that can be hooked
+		attr_reader :opcode_map
+		@opcode_map = {}
+
+
+		### Declare one or more opcodes in the form:
+		###
+		### {
+		###     <bit> => <label>,
+		### }
+		def opcodes( hash )
+			@opcode_map ||= {}
+			@opcode_map.merge!( hash )
+			@opcode_map.each do |bit, label|
+				self.log.debug "Set opcode %p to %#0x" % [ label, bit ]
+				declarative = "on_#{label}"
+				block = self.make_declarative( label )
+				self.log.debug "  declaring method %p on %p" % [ declarative, self ]
+				self.class.send( :define_method, declarative, &block )
+			end
+		end
+
+
+		### Make a declarative method for setting the callback for frames with the specified
+		### +opcode+ (Symbol).
+		def make_declarative( opcode )
+			self.log.debug "Making a declarative for %p" % [ opcode ]
+			return lambda do |&block|
+				self.log.debug "Setting handler for %p frames to %p" % [ opcode, block ]
+				methodname = "on_#{opcode}_frame"
+				define_method( methodname, &block )
+				self.op_callbacks[ opcode ] = self.instance_method( methodname )
+			end
+		end
+
+
+		### Inheritance hook -- inheriting classes inherit their parents' routes table.
+		def inherited( subclass )
+			super
+			subclass.instance_variable_set( :@opcode_map, self.opcode_map.dup )
+			subclass.instance_variable_set( :@op_callbacks, self.op_callbacks.dup )
+		end
+
+
+		### Extension callback -- install default opcode declaratives when the plugin
+		### is registered.
+		def self::extended( mod )
+			super
+			mod.opcodes( Mongrel2::WebSocket::Constants::OPCODE_NAME )
+		end
+
+	end # module ClassMethods
+
+
+
+	### Dispatch the incoming frame to its handler based on its opcode
+	def handle_frame( frame )
+		self.log.debug "[:routing] Opcode map is: %p" % [ self.class.opcode_map ]
+		opname = self.class.opcode_map[ frame.numeric_opcode ]
+		self.log.debug "[:routing] Routing frame: %p" % [ opname ]
+
+		handler = self.class.op_callbacks[ opname ] or return super
+
+		return handler.bind( self ).call( frame )
+	end
+
+end # module Strelka::WebSocketServer::Routing
+

File spec/lib/constants.rb

 ### A collection of constants used in testing
 module Strelka::TestConstants # :nodoc:all
 
-	include Strelka::Constants
+	include Strelka::Constants,
+	        Mongrel2::WebSocket::Constants
 
 	unless defined?( TEST_HOST )
 
 
 		# Freeze all testing constants
 		constants.each do |cname|
-			const_get(cname).freeze
+			const_get(cname).freeze if cname.to_s.start_with?( 'TEST_' )
 		end
 	end
 

File spec/strelka/app/auth_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	it "gives including apps a default authprovider" do

File spec/strelka/app/errors_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "an including App" do

File spec/strelka/app/filters_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "an including App" do

File spec/strelka/app/negotiation_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "an including App" do

File spec/strelka/app/parameters_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "an including App" do

File spec/strelka/app/restresources_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "included in an App" do

File spec/strelka/app/routing_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "an including App" do

File spec/strelka/app/sessions_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "session-class loading" do

File spec/strelka/app/templating_spec.rb

 	end
 
 
-	it_should_behave_like( "A Strelka::App Plugin" )
+	it_should_behave_like( "A Strelka Plugin" )
 
 
 	describe "template discovery" do

File spec/strelka/app_spec.rb

 	# Examples
 	#
 
-	it "has a method for loading app class/es from a file" do
-		app_file = 'an_app.rb'
-		app_path = Pathname( app_file ).expand_path
-		app_class = nil
-
-		Kernel.should_receive( :load ).with( app_path.to_s ).and_return do
-			app_class = Class.new( Strelka::App )
-		end
-		Strelka::App.load( app_file ).should == [ app_class ]
-	end
-
-	it "has a method for discovering installed Strelka app files" do
-		specs = {}
-		specs[:donkey]     = make_gemspec( 'donkey',  '1.0.0' )
-		specs[:rabbit_old] = make_gemspec( 'rabbit',  '1.0.0' )
-		specs[:rabbit_new] = make_gemspec( 'rabbit',  '1.0.8' )
-		specs[:bear]       = make_gemspec( 'bear',    '1.0.0', false )
-		specs[:giraffe]    = make_gemspec( 'giraffe', '1.0.0' )
-
-		expectation = Gem::Specification.should_receive( :each )
-		specs.values.each {|spec| expectation.and_yield(spec) }
-
-		donkey_path  = specs[:donkey].full_gem_path
-		rabbit_path  = specs[:rabbit_new].full_gem_path
-		giraffe_path = specs[:giraffe].full_gem_path
-
-		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
-			and_return( [] )
-		Dir.should_receive( :glob ).with( "#{giraffe_path}/data/giraffe/{apps,handlers}/**/*" ).
-			and_return([ "#{giraffe_path}/data/giraffe/apps/app" ])
-		Dir.should_receive( :glob ).with( "#{rabbit_path}/data/rabbit/{apps,handlers}/**/*" ).
-			and_return([ "#{rabbit_path}/data/rabbit/apps/subdir/app1.rb",
-			             "#{rabbit_path}/data/rabbit/apps/subdir/app2.rb" ])
-		Dir.should_receive( :glob ).with( "#{donkey_path}/data/donkey/{apps,handlers}/**/*" ).
-			and_return([ "#{donkey_path}/data/donkey/apps/app.rb" ])
-
-		app_paths = Strelka::App.discover_paths
-
-		# app_paths.should have( 4 ).members
-		app_paths.should include(
-			'donkey'  => [Pathname("#{donkey_path}/data/donkey/apps/app.rb")],
-			'rabbit'  => [Pathname("#{rabbit_path}/data/rabbit/apps/subdir/app1.rb"),
-			              Pathname("#{rabbit_path}/data/rabbit/apps/subdir/app2.rb")],
-			'giraffe' => [Pathname("#{giraffe_path}/data/giraffe/apps/app")]
-		)
-	end
-
-	it "has a method for loading discovered app classes from installed Strelka app files" do
-		gemspec = make_gemspec( 'blood-orgy', '0.0.3' )
-		Gem::Specification.should_receive( :each ).and_yield( gemspec ).at_least( :once )
-
-		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
-			and_return( [] )
-		Dir.should_receive( :glob ).with( "#{gemspec.full_gem_path}/data/blood-orgy/{apps,handlers}/**/*" ).
-			and_return([ "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ])
-
-		Kernel.stub( :load ).
-			with( "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ).
-			and_return do
-				Class.new( Strelka::App )
-				true
-			end
-
-		app_classes = Strelka::App.discover
-		app_classes.should have( 1 ).member
-		app_classes.first.should be_a( Class )
-		app_classes.first.should < Strelka::App
-	end
-
-	it "handles exceptions while loading discovered apps" do
-		gemspec = make_gemspec( 'blood-orgy', '0.0.3' )
-		Gem::Specification.should_receive( :each ).and_yield( gemspec ).at_least( :once )
-
-		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
-			and_return( [] )
-		Dir.should_receive( :glob ).with( "#{gemspec.full_gem_path}/data/blood-orgy/{apps,handlers}/**/*" ).
-			and_return([ "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ])
-
-		Kernel.stub( :load ).
-			with( "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ).
-			and_raise( SyntaxError.new("kurzweil:1: syntax error, unexpected coffeeshop philosopher") )
-
-		app_classes = Strelka::App.discover
-		app_classes.should be_empty()
-	end
-
-
 	it "returns a No Content response by default" do
 		res = @app.new.handle( @req )
 
 		end
 	end
 
-	it "uses the default local data directory if the config is present without that key" do
-		config = Configurability::Config.new( 'devmode: true' )
-		@app.configure( config )
-		@app.local_data_dirs.should == Strelka::App::CONFIG_DEFAULTS[:local_data_dirs]
-	end
-
 	it "closes async uploads with a 413 Request Entity Too Large by default" do
 		@req.headers.x_mongrel2_upload_start = 'an/uploaded/file/path'
 

File spec/strelka/discovery_spec.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+BEGIN {
+	require 'pathname'
+	basedir = Pathname.new( __FILE__ ).dirname.parent.parent
+	$LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
+}
+
+require 'rspec'
+require 'zmq'
+require 'mongrel2'
+
+require 'spec/lib/helpers'
+
+require 'strelka'
+require 'strelka/discovery'
+
+
+#####################################################################
+###	C O N T E X T S
+#####################################################################
+
+describe Strelka::Discovery do
+
+	before( :all ) do
+		setup_logging()
+		Mongrel2::Config.db = Mongrel2::Config.in_memory_db
+		Mongrel2::Config.init_database
+
+		# Skip loading the 'strelka' gem, which probably doesn't exist in the right version
+		# in the dev environment
+		strelkaspec = make_gemspec( 'strelka', Strelka::VERSION, false )
+		loaded_specs = Gem.instance_variable_get( :@loaded_specs )
+		loaded_specs['strelka'] = strelkaspec
+
+	end
+
+	after( :all ) do
+		reset_logging()
+	end
+
+
+	let( :discoverable_class ) { Class.new {extend Strelka::Discovery} }
+
+	#
+	# Examples
+	#
+
+	it "has a method for loading app class/es from a file" do
+
+		app_file = 'an_app.rb'
+		app_path = Pathname( app_file ).expand_path
+		app_class = nil
+
+		Kernel.should_receive( :load ).with( app_path.to_s ).and_return do
+			app_class = Class.new( discoverable_class )
+		end
+		described_class.load( app_file ).should == [ app_class ]
+	end
+
+	it "has a method for discovering installed Strelka app files" do
+		specs = {}
+		specs[:donkey]     = make_gemspec( 'donkey',  '1.0.0' )
+		specs[:rabbit_old] = make_gemspec( 'rabbit',  '1.0.0' )
+		specs[:rabbit_new] = make_gemspec( 'rabbit',  '1.0.8' )
+		specs[:bear]       = make_gemspec( 'bear',    '1.0.0', false )
+		specs[:giraffe]    = make_gemspec( 'giraffe', '1.0.0' )
+
+		expectation = Gem::Specification.should_receive( :each )
+		specs.values.each {|spec| expectation.and_yield(spec) }
+
+		donkey_path  = specs[:donkey].full_gem_path
+		rabbit_path  = specs[:rabbit_new].full_gem_path
+		giraffe_path = specs[:giraffe].full_gem_path
+
+		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
+			and_return( [] )
+		Dir.should_receive( :glob ).with( "#{giraffe_path}/data/giraffe/{apps,handlers}/**/*" ).
+			and_return([ "#{giraffe_path}/data/giraffe/apps/app" ])
+		Dir.should_receive( :glob ).with( "#{rabbit_path}/data/rabbit/{apps,handlers}/**/*" ).
+			and_return([ "#{rabbit_path}/data/rabbit/apps/subdir/app1.rb",
+			             "#{rabbit_path}/data/rabbit/apps/subdir/app2.rb" ])
+		Dir.should_receive( :glob ).with( "#{donkey_path}/data/donkey/{apps,handlers}/**/*" ).
+			and_return([ "#{donkey_path}/data/donkey/apps/app.rb" ])
+
+		app_paths = described_class.discover_paths
+
+		# app_paths.should have( 4 ).members
+		app_paths.should include(
+			'donkey'  => [Pathname("#{donkey_path}/data/donkey/apps/app.rb")],
+			'rabbit'  => [Pathname("#{rabbit_path}/data/rabbit/apps/subdir/app1.rb"),
+			              Pathname("#{rabbit_path}/data/rabbit/apps/subdir/app2.rb")],
+			'giraffe' => [Pathname("#{giraffe_path}/data/giraffe/apps/app")]
+		)
+	end
+
+	it "has a method for loading discovered app classes from installed Strelka app files" do
+		gemspec = make_gemspec( 'blood-orgy', '0.0.3' )
+		Gem::Specification.should_receive( :each ).and_yield( gemspec ).at_least( :once )
+
+		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
+			and_return( [] )
+		Dir.should_receive( :glob ).with( "#{gemspec.full_gem_path}/data/blood-orgy/{apps,handlers}/**/*" ).
+			and_return([ "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ])
+
+		Kernel.stub( :load ).
+			with( "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ).
+			and_return do
+				Class.new( discoverable_class )
+				true
+			end
+
+		app_classes = described_class.discover
+		app_classes.should have( 1 ).member
+		app_classes.first.should be_a( Class )
+		app_classes.first.should < discoverable_class
+	end
+
+	it "handles exceptions while loading discovered apps" do
+		gemspec = make_gemspec( 'blood-orgy', '0.0.3' )
+		Gem::Specification.should_receive( :each ).and_yield( gemspec ).at_least( :once )
+
+		Dir.should_receive( :glob ).with( 'data/*/{apps,handlers}/**/*' ).
+			and_return( [] )
+		Dir.should_receive( :glob ).with( "#{gemspec.full_gem_path}/data/blood-orgy/{apps,handlers}/**/*" ).
+			and_return([ "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ])
+
+		Kernel.stub( :load ).
+			with( "#{gemspec.full_gem_path}/data/blood-orgy/apps/kurzweil" ).
+			and_raise( SyntaxError.new("kurzweil:1: syntax error, unexpected coffeeshop philosopher") )
+
+		app_classes = Strelka::Discovery.discover
+		app_classes.should be_empty()
+	end
+
+
+end
+

File spec/strelka/websocketserver/routing_spec.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+BEGIN {
+	require 'pathname'
+	basedir = Pathname.new( __FILE__ ).dirname.parent.parent.parent
+	$LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
+}
+
+require 'rspec'
+
+require 'spec/lib/helpers'
+
+require 'strelka'
+require 'strelka/plugins'
+require 'strelka/websocketserver/routing'
+
+require 'strelka/behavior/plugin'
+
+
+#####################################################################
+###	C O N T E X T S
+#####################################################################
+
+describe Strelka::WebSocketServer::Routing do
+
+	before( :all ) do
+		setup_logging()
+		@frame_factory = Mongrel2::WebSocketFrameFactory.new( route: '/chat' )
+	end
+
+	after( :all ) do
+		reset_logging()
+	end
+
+
+	it_should_behave_like( "A Strelka Plugin" )
+
+
+	describe "an including App" do
+
+		before( :each ) do
+			@app = Class.new( Strelka::WebSocketServer ) do
+				plugin :routing
+				def initialize( appid='chat-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
+					super
+				end
+			end
+		end
+
+
+		it "has an Hash of raw routes" do
+			@app.op_callbacks.should be_a( Hash )
+		end
+
+		it "knows what its route methods are" do
+			@app.op_callbacks.should == {}
+			@app.class_eval do
+				on_text() {}
+				on_binary() {}
+				on_ping() {}
+			end
+
+			@app.op_callbacks.keys.should == [ :text, :binary, :ping ]
+		end
+
+		it "allows the declaration of custom opcodes" do
+			@app.opcodes( 0x3 => :nick )
+			@app.class_eval do
+				on_nick() {}
+			end
+			@app.op_callbacks.should have( 1 ).member
+			@app.op_callbacks[ :nick ].should be_a( UnboundMethod )
+		end
+
+
+		it "dispatches TEXT frames to a text handler if one is declared" do
+			@app.class_eval do
+				on_text do |frame|
+					res = frame.response
+					res.puts( "#{frame.body.read} Yep!" )
+					return res
+				end
+			end
+
+			frame = @frame_factory.text( "/chat", "Clowns?" )
+			response = @app.new.handle_websocket( frame )
+
+			response.should be_a( Mongrel2::WebSocket::Frame )
+			response.opcode.should == :text
+			response.body.rewind
+			response.body.read.should == "Clowns? Yep!\n"
+		end
+
+		it "dispatches custom frame type to its handler if one is declared" do
+			@app.class_eval do
+				opcodes 0xB => :refresh
+
+				on_refresh do |frame|
+					return frame.response
+				end
+			end
+
+			frame = @frame_factory.create( '/chat', '', 0xB )
+			response = @app.new.handle_websocket( frame )
+
+			response.should be_a( Mongrel2::WebSocket::Frame )
+			response.opcode.should == :reserved
+			response.numeric_opcode.should == 0xB
+		end
+
+
+		it "falls back to the defaults if a handler isn't declared for a frame" do
+			frame = @frame_factory.text( '/chat', '' )
+			response = @app.new.handle_websocket( frame )
+
+			response.should be_a( Mongrel2::WebSocket::Frame )
+			response.opcode.should == :close
+		end
+
+	end
+
+end
+

File spec/strelka/websocketserver_spec.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+# encoding: utf-8
+
+BEGIN {
+	require 'pathname'
+	basedir = Pathname.new( __FILE__ ).dirname.parent.parent
+	$LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
+}
+
+require 'rspec'
+require 'zmq'
+require 'mongrel2'
+
+require 'spec/lib/helpers'
+
+require 'strelka'
+require 'strelka/websocketserver'
+
+
+#####################################################################
+###	C O N T E X T S
+#####################################################################
+
+describe Strelka::WebSocketServer do
+
+	before( :all ) do
+		setup_logging( :fatal )
+		@initial_registry = described_class.loaded_plugins.dup
+		@frame_factory = Mongrel2::WebSocketFrameFactory.new( route: '/chat' )
+		Mongrel2::Config.db = Mongrel2::Config.in_memory_db
+		Mongrel2::Config.init_database
+
+		# Skip loading the 'strelka' gem, which probably doesn't exist in the right version
+		# in the dev environment
+		strelkaspec = make_gemspec( 'strelka', Strelka::VERSION, false )
+		loaded_specs = Gem.instance_variable_get( :@loaded_specs )
+		loaded_specs['strelka'] = strelkaspec
+
+	end
+
+	before( :each ) do
+		described_class.loaded_plugins.clear
+		@app = Class.new( described_class ) do
+			def initialize( appid=TEST_APPID, sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
+				super
+			end
+			def set_signal_handlers; end
+			def start_accepting_requests; end
+			def restore_signal_handlers; end
+		end
+	end
+
+	after( :each ) do
+		@app = nil
+	end
+
+	after( :all ) do
+		described_class.loaded_plugins = @initial_registry
+		reset_logging()
+	end
+
+
+	#
+	# Control frame defaults
+	#
+
+	it "returns a PONG for PING frames" do
+		ping = @frame_factory.ping( '/chat' )
+		res = @app.new.handle_websocket( ping )
+
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :pong
+		res.socket_id.should == ping.socket_id
+	end
+
+	it "ignores PONG frames" do
+		pong = @frame_factory.pong( '/chat' )
+		res = @app.new.handle_websocket( pong )
+
+		res.should be_nil()
+	end
+
+	it "closes the connection on CLOSE frames" do
+		app = @app.new
+		close = @frame_factory.close( '/chat' )
+
+		app.conn.should_receive( :reply_close ).with( close )
+
+		res = app.handle_websocket( close )
+		res.should be_nil()
+	end
+
+	it "closes the connection with an appropriate error for reserved control opcodes" do
+		reserved = @frame_factory.create( '/chat', '', 0xB )
+		res = @app.new.handle_websocket( reserved )
+
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :close
+		res.payload.rewind
+		res.payload.read.should =~ /Unhandled data type/i
+		res.socket_id.should == reserved.socket_id
+	end
+
+	#
+	# Content frame defaults
+	#
+
+	it "replies with a close frame with a bad data type error for TEXT frames" do
+		app = @app.new
+		frame = @frame_factory.text( '/chat' )
+
+		res = app.handle_websocket( frame )
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :close
+		res.payload.rewind
+		res.payload.read.should =~ /Unhandled data type/i
+	end
+
+	it "replies with a close frame with a bad data type error for BINARY frames" do
+		app = @app.new
+		frame = @frame_factory.binary( '/chat' )
+
+		res = app.handle_websocket( frame )
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :close
+		res.payload.rewind
+		res.payload.read.should =~ /Unhandled data type/i
+	end
+
+	it "replies with a close frame with a bad data type error for CONTINUATION frames" do
+		app = @app.new
+		frame = @frame_factory.continuation( '/chat' )
+
+		res = app.handle_websocket( frame )
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :close
+		res.payload.rewind
+		res.payload.read.should =~ /Unhandled data type/i
+	end
+
+	it "closes the connection with an appropriate error for reserved content opcodes" do
+		reserved = @frame_factory.create( '/chat', '', 0x3 )
+		res = @app.new.handle_websocket( reserved )
+
+		res.should be_a( Mongrel2::WebSocket::Frame )
+		res.opcode.should == :close
+		res.payload.rewind
+		res.payload.read.should =~ /Unhandled data type/i
+		res.socket_id.should == reserved.socket_id
+	end
+
+
+end
+