mongrel2 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data.tar.gz.sig CHANGED
Binary file
data/ChangeLog CHANGED
@@ -1,5 +1,40 @@
1
+ 2012-06-30 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ * Manifest.txt, README.rdoc, bin/m2sh.rb, data/mongrel2/config.rb.in,
4
+ lib/mongrel2/config.rb, lib/mongrel2/constants.rb:
5
+ Adding 'bootstrap' and 'quickstart' commands to m2sh.rb.
6
+
7
+ Also adding documentation for them to the README, and moved the
8
+ DATA_DIR constants into mongrel2/constants.
9
+ [2f2b9b4bc86f] [tip]
10
+
11
+ 2012-06-27 Michael Granger <ged@FaerieMUD.org>
12
+
13
+ * examples/.env, examples/Procfile, examples/config.rb, examples/ws-
14
+ echo.rb, lib/mongrel2/handler.rb, lib/mongrel2/httpresponse.rb,
15
+ lib/mongrel2/testing.rb, lib/mongrel2/websocket.rb,
16
+ spec/lib/constants.rb, spec/lib/helpers.rb,
17
+ spec/mongrel2/handler_spec.rb, spec/mongrel2/httpresponse_spec.rb,
18
+ spec/mongrel2/websocket_spec.rb:
19
+ Updating websocket support for recent Mongrel2 changes
20
+ [c17bbae10985]
21
+
1
22
  2012-06-26 Michael Granger <ged@FaerieMUD.org>
2
23
 
24
+ * .hgtags:
25
+ Added tag v0.26.0 for changeset 7fb25e6e09bc
26
+ [1cf11a8e5402]
27
+
28
+ * .hgsigs:
29
+ Added signature for changeset c2eac469ca66
30
+ [7fb25e6e09bc] [v0.26.0]
31
+
32
+ * History.rdoc, lib/mongrel2.rb, spec/lib/helpers.rb,
33
+ spec/mongrel2/handler_spec.rb, spec/mongrel2/request_spec.rb:
34
+ Fix the specs broken by the async upload changes, bump minor
35
+ version, update history.
36
+ [c2eac469ca66]
37
+
3
38
  * lib/mongrel2/config/handler.rb, lib/mongrel2/config/host.rb,
4
39
  lib/mongrel2/config/route.rb, lib/mongrel2/config/server.rb,
5
40
  lib/mongrel2/connection.rb, lib/mongrel2/handler.rb,
@@ -7,7 +42,7 @@
7
42
  spec/mongrel2/handler_spec.rb, spec/mongrel2/httprequest_spec.rb,
8
43
  spec/mongrel2/request_spec.rb:
9
44
  Fix the async upload body path
10
- [782174dcba2e] [tip]
45
+ [782174dcba2e]
11
46
 
12
47
  2012-06-21 Michael Granger <ged@FaerieMUD.org>
13
48
 
@@ -1,3 +1,9 @@
1
+ == v0.27.0 [2012-07-02] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ - Adds support for websocket handshake in 'develop' branch.
4
+ - Adds 'bootstrap' and 'quickstart' commands to m2sh.rb.
5
+
6
+
1
7
  == v0.26.0 [2012-06-26] Michael Granger <ged@FaerieMUD.org>
2
8
 
3
9
  - Fix the derived path to the async upload body
@@ -8,6 +8,7 @@ README.rdoc
8
8
  Rakefile
9
9
  bin/m2sh.rb
10
10
  data/mongrel2/bootstrap.html
11
+ data/mongrel2/config.rb.in
11
12
  data/mongrel2/config.sql
12
13
  data/mongrel2/css/master.css
13
14
  data/mongrel2/js/websock-test.js
@@ -17,29 +17,36 @@ databases in pure Ruby, a Control port interface object, and handler classes
17
17
  for creating applications or higher-level frameworks.
18
18
 
19
19
 
20
- == Installation
20
+ == Installation and Setup
21
21
 
22
- gem install mongrel2
22
+ Install mongrel2:
23
23
 
24
+ $ {brew,port,portmaster,apt-get install,etc} mongrel2
24
25
 
25
- This library uses Jeremy Hinegardner's 'amalgalite' library for the config ORM
26
- classes, but it will also fall back to using the sqlite3 library instead:
26
+ Install the mongrel2 gem:
27
27
 
28
- # Loading the sqlite3 library explicitly
29
- $ rspec -rsqlite3 -cfp spec
30
- >>> Using SQLite3 1.3.4 for DB access.
31
- .....[...]
28
+ $ gem install mongrel2
32
29
 
33
- Finished in 5.53 seconds
34
- 102 examples, 0 failures
30
+ Dump a config database generation script into the current working directory:
35
31
 
36
- # No -rsqlite3 means amalgalite loads first.
37
- $ rspec -cfp spec
38
- >>> Using Amalgalite 1.1.2 for DB access.
39
- .....[...]
32
+ $ m2sh.rb bootstrap
40
33
 
41
- Finished in 3.67 seconds
42
- 102 examples, 0 failures
34
+ Edit the generated file:
35
+
36
+ $ $EDITOR config.rb
37
+
38
+ Create a config database from the Ruby config:
39
+
40
+ $ m2sh.rb load config.rb
41
+
42
+ Start the server:
43
+
44
+ $ m2sh.rb start
45
+
46
+ Or combine <tt>bootstrap</tt>, <tt>load</tt>, and <tt>start</tt> all into one
47
+ command:
48
+
49
+ $ m2sh.rb quickstart
43
50
 
44
51
 
45
52
  == Usage
@@ -54,6 +61,26 @@ for adding the Ruby configuration DSL to your namespace, and the top-level
54
61
  Mongrel2::Config class, which manages the database connection, installs the
55
62
  schema, etc.
56
63
 
64
+ The ORM classes use Jeremy Hinegardner's 'amalgalite' library, but it will
65
+ also fall back to using the sqlite3 library instead:
66
+
67
+ # Loading the sqlite3 library explicitly
68
+ $ rspec -rsqlite3 -cfp spec
69
+ >>> Using SQLite3 1.3.4 for DB access.
70
+ .....[...]
71
+
72
+ Finished in 5.53 seconds
73
+ 102 examples, 0 failures
74
+
75
+ # No -rsqlite3 means amalgalite loads first.
76
+ $ rspec -cfp spec
77
+ >>> Using Amalgalite 1.1.2 for DB access.
78
+ .....[...]
79
+
80
+ Finished in 3.67 seconds
81
+ 102 examples, 0 failures
82
+
83
+
57
84
  * Mongrel2::Config
58
85
  * Mongrel2::Config::DSL
59
86
  * Mongrel2::Config::Server
@@ -82,6 +109,7 @@ Mongrel2 sends:
82
109
  * Mongrel2::HTTPRequest
83
110
  * Mongrel2::JSONRequest
84
111
  * Mongrel2::XMLRequest
112
+ * Mongrel2::WebSocket::ClientHandshake
85
113
  * Mongrel2::WebSocket::Frame
86
114
 
87
115
  These are all {overridable}[rdoc-ref:Mongrel2::Request.register_request_type]
@@ -94,17 +122,20 @@ handlers.
94
122
  === The Control Class
95
123
 
96
124
  The Mongrel2::Control class is an object interface to {the Mongrel2 control
97
- port}[http://mongrel2.org/static/mongrel2-manual.html#x1-390003.8]. It can be
125
+ port}[http://mongrel2.org/static/book-finalch4.html#x6-390003.8]. It can be
98
126
  used to stop and restart the server, check its status, etc.
99
127
 
100
128
 
101
129
  === Other Classes
102
130
 
103
- There are a few other classes and modules work checking out, too:
131
+ There are a few other classes and modules worth checking out, too:
104
132
 
105
- * Mongrel2::Table
106
- * Mongrel2::Constants
107
- * Mongrel2::Loggable
133
+ Mongrel2::Table::
134
+ A hash-like data structure for headers, etc.
135
+ Mongrel2::Constants::
136
+ A collection of convenience constants for Mongrel2 handlers.
137
+ Mongrel2::RequestFactory::
138
+ A factory for generating fixtured requests of various types for testing.
108
139
 
109
140
 
110
141
  == Contributing
@@ -186,7 +186,7 @@ class Mongrel2::M2SHCommand
186
186
  text ''
187
187
 
188
188
  text 'Global Options'
189
- opt :config, "Specify the configfile to use.",
189
+ opt :config, "Specify the config database to use.",
190
190
  :default => DEFAULT_CONFIG_URI
191
191
  opt :sudo, "Use 'sudo' to run the mongrel2 server."
192
192
  opt :port, "Reset the server port to <i> before starting it.",
@@ -588,6 +588,43 @@ class Mongrel2::M2SHCommand
588
588
  usage :running, "[server]"
589
589
 
590
590
 
591
+ ### The 'bootstrap' command.
592
+ def bootstrap_command( *args )
593
+ scriptname = args.shift || DEFAULT_CONFIG_SCRIPT
594
+ template = Mongrel2::DATA_DIR + 'config.rb.in'
595
+ data = template.read
596
+
597
+ data.gsub!( /%% PWD %%/, Dir.pwd )
598
+
599
+ header "Writing a config-generation script to %s" % [ scriptname ]
600
+ File.open( scriptname, File::WRONLY|File::EXCL|File::CREAT, 0755, encoding: 'utf-8' ) do |fh|
601
+ fh.print( data )
602
+ end
603
+ message "Done."
604
+ end
605
+ help :bootstrap, "Generate a basic config-generation script."
606
+ usage :boostrap, "[scriptname]"
607
+
608
+
609
+ ### The 'quickstart' command.
610
+ def quickstart_command( *args )
611
+ configfile = 'config.rb'
612
+
613
+ header "Quickstart!"
614
+ self.bootstrap_command( configfile )
615
+ edit( configfile )
616
+ self.load_command( configfile )
617
+
618
+ message '---'
619
+ header "Point a browser at: "
620
+ message '---'
621
+
622
+ self.start_command()
623
+ end
624
+ help :quickstart, "Set up a basic mongrel2 server and run it."
625
+ usage :quickstart
626
+
627
+
591
628
  ### The 'version' command
592
629
  def version_command( *args )
593
630
  message( "<%= color 'Version:', :header %> " + Mongrel2.version_string(true) )
@@ -599,6 +636,10 @@ class Mongrel2::M2SHCommand
599
636
  # Utility methods
600
637
  #
601
638
 
639
+ #######
640
+ private
641
+ #######
642
+
602
643
  ### Output normal output
603
644
  def message( *parts )
604
645
  self.prompt.say( parts.map(&:to_s).join($/) )
@@ -676,6 +717,19 @@ class Mongrel2::M2SHCommand
676
717
  return server
677
718
  end
678
719
 
720
+
721
+ ### Invoke the user's editor on the given +filename+ and return the exit code
722
+ ### from doing so.
723
+ def edit( filename )
724
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
725
+ system editor, filename.to_s
726
+ unless $?.success? || editor =~ /vim/i
727
+ raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
728
+ end
729
+ end
730
+
731
+
732
+
679
733
  end # class Mongrel2::M2SHCommand
680
734
 
681
735
 
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ #encoding: utf-8
3
+
4
+ require 'pathname'
5
+ require 'tmpdir'
6
+
7
+ # This is a Ruby script that will *generate* the SQLite database
8
+ # that Mongrel2 uses for its configuration. You can just as easily
9
+ # use Mongrel2's 'm2sh' and the Pythonish config syntax described
10
+ # in the manual if you prefer that.
11
+ #
12
+ # See the "Mongrel2 Config DSL" section of the API docs, and the "How A
13
+ # Config Is Structured" section of the manual for details
14
+ # on specific items:
15
+ #
16
+ # Mongrel2 Config DSL::
17
+ # http://deveiate.org/code/mongrel2/DSL_rdoc.html
18
+ #
19
+ # How A Config Is Structured::
20
+ # http://mongrel2.org/static/book-finalch4.html#x6-260003.4
21
+ #
22
+ # You can load this via the 'm2sh.rb' tool that comes with the 'mongrel2'
23
+ # gem:
24
+ #
25
+ # m2sh.rb -c config.sqlite load config.rb
26
+
27
+ # Establish some directories
28
+ base_dir = Pathname( '%% PWD %%' )
29
+ upload_dir = Pathname( Dir.tmpdir ) + 'm2spool'
30
+
31
+ # Main Mongrel2 server config
32
+ main = server 'main' do
33
+
34
+ name 'Main'
35
+ default_host 'localhost'
36
+ chroot base_dir
37
+
38
+ # All of these values are relative to the 'chroot' value if Mongrel2
39
+ # is started as root. If it's not, they're relative to the directory
40
+ # it's started in.
41
+ access_log '/logs/access.log'
42
+ error_log '/logs/error.log'
43
+ pid_file '/var/run/mongrel2.pid'
44
+
45
+ # This the address and port the server will listen on. You can
46
+ # use '0.0.0.0' as the bind_addr to listen on all interfaces/IPs.
47
+ bind_addr '127.0.0.1'
48
+ port 8113
49
+
50
+ host 'localhost' do
51
+
52
+ # Serve static content out of a 'public' subdirectory
53
+ route '/', directory( "public/", 'index.html', 'text/html' )
54
+
55
+ # Dynamic content is served via handler routes
56
+ route '/hello', handler( 'tcp://127.0.0.1:9999', 'helloworld' )
57
+
58
+ end
59
+
60
+ end
61
+
62
+ setting 'limits.content_length', 512 * 1024
63
+ setting 'control_port', 'ipc://var/run/mongrel2.sock'
64
+ setting 'upload.temp_store', upload_dir + 'mongrel2.upload.XXXXXX'
65
+
66
+ # Make relative directories so that starting as a regular user works
67
+ (base_dir + "./#{main.access_log}").dirname.mkpath
68
+ (base_dir + "./#{main.error_log}").dirname.mkpath
69
+ (base_dir + "./#{main.pid_file}").dirname.mkpath
70
+ mkdir_p( upload_dir )
71
+
@@ -1 +1,2 @@
1
1
  RUBYOPT=-I../lib
2
+ MONGREL2=/Users/mgranger/source/C/mongrel2/bin/mongrel2
@@ -3,4 +3,4 @@ mongrel2: ruby ../bin/m2sh.rb -c examples.sqlite start
3
3
  helloworld: ruby helloworld-handler.rb
4
4
  async_upload: ruby async-upload.rb
5
5
  request_dumper: ruby request-dumper.rb
6
- ws: ruby ws-echo.rb
6
+ ws: ruby -w ws-echo.rb
@@ -23,7 +23,7 @@ server 'examples' do
23
23
  chroot '/var/mongrel2'
24
24
  pid_file '/var/run/mongrel2.pid'
25
25
 
26
- bind_addr '127.0.0.1'
26
+ bind_addr '0.0.0.0'
27
27
  port 8113
28
28
 
29
29
  # your main host
@@ -86,6 +86,18 @@ class WebSocketEchoServer < Mongrel2::Handler
86
86
  end
87
87
 
88
88
 
89
+ # Handle the initial handshake. Assumes no sub-protocols or protocol version
90
+ # checks are necessary.
91
+ def handle_websocket_handshake( handshake )
92
+ self.log.info "Handshake from %s" % [ handshake.remote_ip ]
93
+
94
+ response = handshake.response( handshake.protocols.first )
95
+ @connections[ [handshake.sender_id, handshake.conn_id] ] = Time.now
96
+
97
+ return response
98
+ end
99
+
100
+
89
101
  # This is the main handler for WebSocket requests. Each frame comes in as a
90
102
  # Mongrel::WebSocket::Frame object, and then is dispatched according to what
91
103
  # opcode it has.
@@ -20,10 +20,10 @@ module Mongrel2
20
20
  abort "\n\n>>> Mongrel2 requires Ruby 1.9.2 or later. <<<\n\n" if RUBY_VERSION < '1.9.2'
21
21
 
22
22
  # Library version constant
23
- VERSION = '0.26.0'
23
+ VERSION = '0.27.0'
24
24
 
25
25
  # Version-control revision constant
26
- REVISION = %q$Revision: c2eac469ca66 $
26
+ REVISION = %q$Revision: 6de3cbe2409c $
27
27
 
28
28
 
29
29
  require 'mongrel2/constants'
@@ -28,6 +28,7 @@ end
28
28
 
29
29
  require 'mongrel2' unless defined?( Mongrel2 )
30
30
  require 'mongrel2/table'
31
+ require 'mongrel2/constants'
31
32
 
32
33
  module Mongrel2
33
34
 
@@ -42,10 +43,11 @@ module Mongrel2
42
43
  # plugins.
43
44
  #
44
45
  # == References
45
- # * http://mongrel2.org/static/mongrel2-manual.html#x1-250003.4
46
+ # * http://mongrel2.org/static/book-finalch4.html#x6-260003.4
46
47
  #
47
48
  class Config < Sequel::Model
48
49
  extend Loggability
50
+ include Mongrel2::Constants
49
51
 
50
52
  # Loggability API -- set up logging under the 'mongrel2' log host
51
53
  log_to :mongrel2
@@ -63,13 +65,6 @@ module Mongrel2
63
65
  }
64
66
  DEFAULTS = CONFIG_DEFAULTS
65
67
 
66
- # The Pathname of the data directory
67
- DATA_DIR = if Gem.datadir( 'mongrel2' )
68
- Pathname( Gem.datadir('mongrel2') )
69
- else
70
- Pathname( __FILE__ ).dirname.parent.parent + 'data/mongrel2'
71
- end
72
-
73
68
  # The Pathname of the SQL file used to create the config database
74
69
  CONFIG_SQL = DATA_DIR + 'config.sql'
75
70
 
@@ -15,7 +15,7 @@ require 'mongrel2' unless defined?( Mongrel2 )
15
15
  # then encodes and sends Mongrel2::Response objects back to the server.
16
16
  #
17
17
  # == References
18
- # * http://mongrel2.org/static/mongrel2-manual.html#x1-700005.3
18
+ # * http://mongrel2.org/static/book-finalch6.html#x8-710005.3
19
19
  class Mongrel2::Connection
20
20
  extend Loggability
21
21
 
@@ -8,9 +8,21 @@ require 'mongrel2' unless defined?( Mongrel2 )
8
8
  # A collection of constants that are shared across the library
9
9
  module Mongrel2::Constants
10
10
 
11
+ # The Pathname of the data directory
12
+ DATA_DIR = if ENV['MONGREL2_DATADIR']
13
+ Pathname( ENV['MONGREL2_DATADIR'] )
14
+ elsif Gem.datadir( 'mongrel2' )
15
+ Pathname( Gem.datadir('mongrel2') )
16
+ else
17
+ Pathname( __FILE__ ).dirname.parent.parent + 'data/mongrel2'
18
+ end
19
+
11
20
  # The path to the default Sqlite configuration database
12
21
  DEFAULT_CONFIG_URI = 'config.sqlite'
13
22
 
23
+ # The default name of the config-generation script
24
+ DEFAULT_CONFIG_SCRIPT = 'config.rb'
25
+
14
26
  # The default URI of the control socket
15
27
  DEFAULT_CONTROL_SOCKET = 'ipc://run/control'
16
28
 
@@ -11,7 +11,7 @@ require 'mongrel2' unless defined?( Mongrel2 )
11
11
  # An interface to the Mongrel2 control port.
12
12
  #
13
13
  # == References
14
- # (http://mongrel2.org/static/mongrel2-manual.html#x1-380003.8)
14
+ # (http://mongrel2.org/static/book-finalch4.html#x6-390003.8)
15
15
  class Mongrel2::Control
16
16
  extend Loggability
17
17
 
@@ -207,19 +207,18 @@ class Mongrel2::Handler
207
207
  return self.handle_async_upload_start( request )
208
208
 
209
209
  else
210
+ self.log.debug "%s request." % [ request.headers['METHOD'] ]
210
211
  case request
212
+ when Mongrel2::WebSocket::ClientHandshake
213
+ return self.handle_websocket_handshake( request )
214
+ when Mongrel2::WebSocket::Frame
215
+ return self.handle_websocket( request )
211
216
  when Mongrel2::HTTPRequest
212
- self.log.debug "HTTP request."
213
217
  return self.handle( request )
214
218
  when Mongrel2::JSONRequest
215
- self.log.debug "JSON message request."
216
219
  return self.handle_json( request )
217
220
  when Mongrel2::XMLRequest
218
- self.log.debug "XML message request."
219
221
  return self.handle_xml( request )
220
- when Mongrel2::WebSocket::Frame
221
- self.log.debug "WEBSOCKET message request."
222
- return self.handle_websocket( request )
223
222
  else
224
223
  self.log.error "Unhandled request type %s (%p)" %
225
224
  [ request.headers['METHOD'], request.class ]
@@ -279,10 +278,24 @@ class Mongrel2::Handler
279
278
  ### Handle a WebSocket frame in +request+. If not overridden, WebSocket connections are
280
279
  ### closed with a policy error status.
281
280
  def handle_websocket( request )
282
- self.log.warn "Unhandled WEBSOCKET message request (%p)" % [ request.headers.path ]
281
+ self.log.warn "Unhandled WEBSOCKET frame (%p)" % [ request.headers.path ]
283
282
  res = request.response
284
283
  res.make_close_frame( WebSocket::CLOSE_POLICY_VIOLATION )
285
- return res
284
+ self.conn.reply( res)
285
+
286
+ self.conn.reply_close( request )
287
+
288
+ return nil
289
+ end
290
+
291
+
292
+ ### Handle a WebSocket handshake HTTP +request+. If not overridden, this method drops
293
+ ### the connection.
294
+ def handle_websocket_handshake( handshake )
295
+ self.log.warn "Unhandled WEBSOCKET_HANDSHAKE request (%p)" % [ request.headers.path ]
296
+ self.conn.reply_close( request )
297
+
298
+ return nil
286
299
  end
287
300
 
288
301
 
@@ -169,12 +169,13 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
169
169
  headers = self.headers.dup
170
170
 
171
171
  headers[:date] ||= Time.now.httpdate
172
- headers[:content_length] ||= self.get_content_length
173
172
 
174
173
  if self.bodiless?
174
+ headers.delete( :content_length )
175
175
  headers.delete( :content_type )
176
176
  else
177
- headers[:content_type] ||= DEFAULT_CONTENT_TYPE.dup
177
+ headers[:content_length] ||= self.get_content_length
178
+ headers[:content_type] ||= DEFAULT_CONTENT_TYPE.dup
178
179
  end
179
180
 
180
181
  return headers
@@ -184,11 +185,11 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
184
185
  ### Get the length of the body IO. If the IO's offset is somewhere other than
185
186
  ### the beginning or end, the size of the remainder is used.
186
187
  def get_content_length
187
- if self.bodiless?
188
- return 0
189
- elsif self.body.pos.nonzero? && !self.body.eof?
188
+ if self.body.pos.nonzero? && !self.body.eof?
189
+ self.log.info "Calculating content length based on an offset of %d" % [ self.body.pos ]
190
190
  return self.body.size - self.body.pos
191
191
  else
192
+ self.log.debug "Calculating body size via %p" % [ self.body.method(:size) ]
192
193
  return self.body.size
193
194
  end
194
195
  end
@@ -12,13 +12,38 @@ require 'mongrel2/request'
12
12
 
13
13
  module Mongrel2
14
14
 
15
- ### A collection of helper functions that are generally useful
16
- ### for testing Mongrel2::Handlers.
15
+ # A collection of helper functions that are generally useful
16
+ # for testing Mongrel2::Handlers.
17
17
  module SpecHelpers
18
18
  end # module SpecHelpers
19
19
 
20
20
 
21
- ### A factory for generating Mongrel2::Request objects for testing.
21
+ # A factory for generating Mongrel2::Request objects for testing.
22
+ #
23
+ # Usage:
24
+ #
25
+ # require 'mongrel2/testing'
26
+ #
27
+ # describe "MyHandler" do
28
+ # before( :all ) do
29
+ # @factory = Mongrel2::RequestFactory.
30
+ # new( sender_id: 'my-handler',
31
+ # route: '/api/v1',
32
+ # headers: {accept: 'application/json'} )
33
+ # end
34
+ #
35
+ # before( :each ) do
36
+ # @app = MyHandler.new( 'my-handler', 'tcp://0.0.0.0:5556',
37
+ # 'tcp://0.0.0.0:5555' )
38
+ # end
39
+ #
40
+ # it "handles a JSON request for GET /" do
41
+ # request = @factory.get( '/api/v1' )
42
+ # response = @app.dispatch_request( request )
43
+ # #...
44
+ # end
45
+ # end
46
+ #
22
47
  class RequestFactory
23
48
  extend Loggability
24
49
 
@@ -218,14 +243,11 @@ module Mongrel2
218
243
  end # RequestFactory
219
244
 
220
245
 
221
- ### A factory for generating WebSocket request objects for testing.
246
+ # A factory for generating WebSocket request objects for testing.
247
+ #
222
248
  class WebSocketFrameFactory < Mongrel2::RequestFactory
223
- extend Loggability
224
249
  include Mongrel2::Constants
225
250
 
226
- # Loggability API -- set up logging under the 'mongrel2' log host
227
- log_to :mongrel2
228
-
229
251
  # The default host
230
252
  DEFAULT_TESTING_HOST = 'localhost'
231
253
  DEFAULT_TESTING_PORT = '8113'
@@ -261,6 +283,8 @@ module Mongrel2
261
283
  :headers => DEFAULT_TESTING_HEADERS,
262
284
  }
263
285
 
286
+ DEFAULT_HANDSHAKE_BODY = 'GR7M5bFPiY2GvVc5a7CIMErQ18Q='
287
+
264
288
  # Freeze all testing constants
265
289
  constants.each do |cname|
266
290
  const_get(cname).freeze
@@ -285,6 +309,26 @@ module Mongrel2
285
309
  public
286
310
  ######
287
311
 
312
+ ### Create an initial websocket handshake request and return it.
313
+ def handshake( uri, *subprotocols )
314
+ raise "Request doesn't route through %p" % [ self.route ] unless
315
+ uri.start_with?( self.route )
316
+
317
+ headers = if subprotocols.last.is_a?( Hash ) then subprotocols.pop else {} end
318
+ headers = self.make_merged_headers( uri, 0, headers )
319
+ headers.delete( :flags )
320
+
321
+ unless subprotocols.empty?
322
+ protos = subprotocols.map( &:to_s ).join( ', ' )
323
+ headers.sec_websocket_protocol = protos
324
+ end
325
+
326
+ rclass = Mongrel2::Request.subclass_for_method( :WEBSOCKET_HANDSHAKE )
327
+
328
+ return rclass.new( self.sender_id, self.conn_id.to_s, self.route, headers, DEFAULT_HANDSHAKE_BODY.dup )
329
+ end
330
+
331
+
288
332
  ### Create a new request with the specified +uri+, +data+, and +flags+.
289
333
  def create( uri, data, *flags )
290
334
  raise "Request doesn't route through %p" % [ self.route ] unless
@@ -9,6 +9,11 @@ require 'mongrel2/constants'
9
9
  #
10
10
  # class WebSocketEchoServer
11
11
  #
12
+ # def handle_websocket_handshake( handshake )
13
+ # # :TODO: Sub-protocol/protocol version checks?
14
+ # return handshake.response
15
+ # end
16
+ #
12
17
  # def handle_websocket( frame )
13
18
  #
14
19
  # # Close connections that send invalid frames
@@ -191,6 +196,93 @@ module Mongrel2::WebSocket
191
196
  # Exception raised when a frame is malformed, doesn't parse, or is otherwise invalid.
192
197
  class FrameError < Mongrel2::WebSocket::Error; end
193
198
 
199
+ # Exception raised when a handshake is created with an unrequested sub-protocol.
200
+ class HandshakeError < Mongrel2::WebSocket::Error; end
201
+
202
+
203
+ # The client (request) handshake for a WebSocket opening handshake.
204
+ class ClientHandshake < Mongrel2::HTTPRequest
205
+ include Mongrel2::WebSocket::Constants
206
+
207
+ # Set this class as the one that will handle WEBSOCKET_HANDSHAKE requests
208
+ register_request_type( self, :WEBSOCKET_HANDSHAKE )
209
+
210
+
211
+ ### Override the type of response returned by this request type. Since
212
+ ### websocket handshakes are symmetrical, responses are just new
213
+ ### Mongrel2::WebSocket::Handshakes with the same Mongrel2 sender
214
+ ### and connection IDs.
215
+ def self::response_class
216
+ return Mongrel2::WebSocket::ServerHandshake
217
+ end
218
+
219
+
220
+ ######
221
+ public
222
+ ######
223
+
224
+ ### The list of protocols in the handshake's Sec-WebSocket-Protocol header
225
+ ### as an Array of Strings.
226
+ def protocols
227
+ return ( self.headers.sec_websocket_protocol || '' ).split( /\s*,\s*/ )
228
+ end
229
+
230
+
231
+ ### Create a Mongrel2::WebSocket::Handshake that will respond to the same server/connection as
232
+ ### the receiver.
233
+ def response( protocol=nil )
234
+ @response = super() unless @response
235
+ if protocol
236
+ raise Mongrel2::WebSocket::HandshakeError,
237
+ "attempt to create a %s handshake which isn't supported by the client." %
238
+ [ protocol ] unless self.protocols.include?( protocol.to_s )
239
+ @response.protocols = protocol
240
+ end
241
+
242
+ return @response
243
+ end
244
+
245
+ end # class ClientHandshake
246
+
247
+
248
+ # The server (response) handshake for a WebSocket opening handshake.
249
+ class ServerHandshake < Mongrel2::HTTPResponse
250
+ include Mongrel2::WebSocket::Constants
251
+
252
+ ### Create a server handshake frame from the given client +handshake+.
253
+ def self::from_request( handshake )
254
+ self.log.debug "Creating the server handshake for client handshake %p" % [ handshake ]
255
+ response = super
256
+ response.body.truncate( 0 )
257
+
258
+ # Mongrel2 puts the negotiated key in the body of the request
259
+ response.headers.sec_websocket_accept = handshake.body.read
260
+
261
+ # Set up the other typical server handshake values
262
+ response.status = HTTP::SWITCHING_PROTOCOLS
263
+ response.header.upgrade = 'websocket'
264
+ response.header.connection = 'Upgrade'
265
+
266
+ return response
267
+ end
268
+
269
+
270
+ ### The list of protocols in the handshake's Sec-WebSocket-Protocol header
271
+ ### as an Array of Strings.
272
+ def protocols
273
+ return ( self.headers.sec_websocket_protocol || '' ).split( /\s*,\s*/ )
274
+ end
275
+
276
+
277
+ ### Set the list of protocols in the handshake's Sec-WebSocket-Protocol header.
278
+ def protocols=( new_protocols )
279
+ value = Array( new_protocols ).join( ', ' )
280
+ self.headers.sec_websocket_protocol = value
281
+ end
282
+
283
+
284
+ end # class ServerHandshake
285
+
194
286
 
195
287
  # WebSocket frame class; this is used for both requests and responses in
196
288
  # WebSocket services.
@@ -207,7 +299,8 @@ module Mongrel2::WebSocket
207
299
 
208
300
  ### Override the type of response returned by this request type. Since
209
301
  ### WebSocket connections are symmetrical, responses are just new
210
- ### WebSocketFrames with the same Mongrel2 sender and connection IDs.
302
+ ### Mongrel2::WebSocket::Frames with the same Mongrel2 sender and
303
+ ### connection IDs.
211
304
  def self::response_class
212
305
  return self
213
306
  end
@@ -448,10 +541,13 @@ module Mongrel2::WebSocket
448
541
  end
449
542
 
450
543
 
451
- ### Create a Mongrel2::Response that will respond to the same server/connection as
452
- ### the receiver. If you wish your specialized Request class to have a corresponding
453
- ### response type, you can override the Mongrel2::Request.response_class method
454
- ### to achieve that.
544
+ ### Create a frame in response to the receiving Frame (i.e., with the same
545
+ ### Mongrel2 connection ID and sender). This automatically sets up the correct
546
+ ### status, Sec-WebSocket-Accept:, Connection, and Upgrade: headers based on the
547
+ ### receiver. If +protocol+ is non-nil, and it matches one of the
548
+ ### values listed in 'Sec-WebSocket-Protocol', it will be set as the
549
+ ### Handshake's Sec-WebSocket-Protocol header. If it is non-nil, but doesn't
550
+ ### match one of the request's values, a Mongrel2::WebSocket::Error is raised.
455
551
  def response( *flags )
456
552
  unless @response
457
553
  @response = super()
@@ -109,6 +109,54 @@ module Mongrel2::TestConstants # :nodoc:all
109
109
  }
110
110
 
111
111
 
112
+ #
113
+ # WebSocket frame constants
114
+ #
115
+
116
+ TEST_WEBSOCKET_PATH = '/ws'
117
+
118
+ TEST_WEBSOCKET_HEADERS = {
119
+ 'connection' => 'Upgrade',
120
+ 'FLAGS' => '0x8A',
121
+ 'host' => 'host.example.com:80',
122
+ 'METHOD' => 'WEBSOCKET',
123
+ 'origin' => 'http://host.example.com:80',
124
+ 'PATH' => TEST_WEBSOCKET_PATH,
125
+ 'PATTERN' => TEST_WEBSOCKET_PATH,
126
+ 'sec-websocket-extensions' => 'x-webkit-deflate-frame',
127
+ 'sec-websocket-key' => 'SQvDVdT2SbgTg6P/lSZo7Q==',
128
+ 'sec-websocket-protocol' => 'echo',
129
+ 'sec-websocket-version' => '13',
130
+ 'upgrade' => 'websocket',
131
+ 'URI' => TEST_WEBSOCKET_PATH,
132
+ 'VERSION' => 'HTTP/1.1',
133
+ 'x-forwarded-for' => '127.0.0.2',
134
+ }
135
+ TEST_WEBSOCKET_HANDSHAKE_HEADERS = {
136
+ 'connection' => 'Upgrade',
137
+ 'host' => 'host.example.com:80',
138
+ 'METHOD' => 'WEBSOCKET_HANDSHAKE',
139
+ 'origin' => 'http://host.example.com:80',
140
+ 'PATH' => TEST_WEBSOCKET_PATH,
141
+ 'PATTERN' => TEST_WEBSOCKET_PATH,
142
+ 'sec-websocket-extensions' => 'x-webkit-deflate-frame',
143
+ 'sec-websocket-key' => 'SQvDVdT2SbgTg6P/lSZo7Q==',
144
+ 'sec-websocket-protocol' => 'echo',
145
+ 'sec-websocket-version' => '13',
146
+ 'upgrade' => 'websocket',
147
+ 'URI' => TEST_WEBSOCKET_PATH,
148
+ 'VERSION' => 'HTTP/1.1',
149
+ 'x-forwarded-for' => '127.0.0.2',
150
+ }
151
+ TEST_WEBSOCKET_BODY = 'GR7M5bFPiY2GvVc5a7CIMErQ18Q='
152
+ TEST_WEBSOCKET_REQUEST_OPTS = {
153
+ :uuid => TEST_UUID,
154
+ :id => TEST_ID,
155
+ :path => TEST_WEBSOCKET_PATH,
156
+ :body => '',
157
+ }
158
+
159
+
112
160
  #
113
161
  # HTTP constants
114
162
  #
@@ -114,13 +114,14 @@ module Mongrel2::SpecHelpers
114
114
  bodystring = TNetstring.dump( opts[:body] || '' )
115
115
 
116
116
  # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
117
- return "%s %d %s %s%s" % [
117
+ data = "%s %d %s %s%s" % [
118
118
  opts[:uuid],
119
119
  opts[:id],
120
120
  opts[:path],
121
121
  headerstring,
122
122
  bodystring,
123
123
  ]
124
+ return data.encode( 'binary' )
124
125
  end
125
126
 
126
127
 
@@ -134,13 +135,14 @@ module Mongrel2::SpecHelpers
134
135
  bodystring = TNetstring.dump( opts[:body] || '' )
135
136
 
136
137
  # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
137
- return "%s %d %s %s%s" % [
138
+ data = "%s %d %s %s%s" % [
138
139
  opts[:uuid],
139
140
  opts[:id],
140
141
  opts[:path],
141
142
  headerstring,
142
143
  bodystring,
143
144
  ]
145
+ return data.encode( 'binary' )
144
146
  end
145
147
 
146
148
 
@@ -156,13 +158,14 @@ module Mongrel2::SpecHelpers
156
158
  bodystring = TNetstring.dump( Yajl::Encoder.encode(opts[:body] || []) )
157
159
 
158
160
  # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
159
- return "%s %d %s %s%s" % [
161
+ data = "%s %d %s %s%s" % [
160
162
  opts[:uuid],
161
163
  opts[:id],
162
164
  opts[:path],
163
165
  headerstring,
164
166
  bodystring,
165
167
  ]
168
+ return data.encode( 'binary' )
166
169
  end
167
170
 
168
171
 
@@ -178,13 +181,56 @@ module Mongrel2::SpecHelpers
178
181
  bodystring = TNetstring.dump( opts[:body] || "#{TEST_XML_PATH} />" )
179
182
 
180
183
  # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
181
- return "%s %d %s %s%s" % [
184
+ data = "%s %d %s %s%s" % [
182
185
  opts[:uuid],
183
186
  opts[:id],
184
187
  opts[:path],
185
188
  headerstring,
186
189
  bodystring,
187
190
  ]
191
+ return data.encode( 'binary' )
192
+ end
193
+
194
+ ### Make a Mongrel2 handshake request for a WebSocket route.
195
+ def make_websocket_handshake( opts={} )
196
+ opts = TEST_WEBSOCKET_REQUEST_OPTS.merge( opts )
197
+ headers = normalize_headers( opts, TEST_WEBSOCKET_HANDSHAKE_HEADERS )
198
+
199
+ Mongrel2.log.debug "WebSocket start handshake, headers = %p, opts = %p" % [ headers, opts ]
200
+
201
+ headerstring = TNetstring.dump( Yajl::Encoder.encode(headers) )
202
+ bodystring = TNetstring.dump( opts[:body] || TEST_WEBSOCKET_BODY )
203
+
204
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
205
+ data = "%s %d %s %s%s" % [
206
+ opts[:uuid],
207
+ opts[:id],
208
+ opts[:path],
209
+ headerstring,
210
+ bodystring,
211
+ ]
212
+ return data.encode( 'binary' )
213
+ end
214
+
215
+ ### Make a Mongrel2 frame for a WebSocket route.
216
+ def make_websocket_frame( opts={} )
217
+ opts = TEST_WEBSOCKET_REQUEST_OPTS.merge( opts )
218
+ headers = normalize_headers( opts, TEST_WEBSOCKET_HEADERS )
219
+
220
+ Mongrel2.log.debug "WebSocket frame, headers = %p, opts = %p" % [ headers, opts ]
221
+
222
+ headerstring = TNetstring.dump( Yajl::Encoder.encode(headers) )
223
+ bodystring = TNetstring.dump( opts[:body] )
224
+
225
+ # UUID ID PATH SIZE:HEADERS,SIZE:BODY,
226
+ data = "%s %d %s %s%s" % [
227
+ opts[:uuid],
228
+ opts[:id],
229
+ opts[:path],
230
+ headerstring,
231
+ bodystring,
232
+ ]
233
+ return data.encode( 'binary' )
188
234
  end
189
235
 
190
236
  end
@@ -209,6 +209,42 @@ describe Mongrel2::Handler do
209
209
  response.should be_a( Mongrel2::Response )
210
210
  end
211
211
 
212
+ it "dispatches WebSocket opening handshakes to the #handle_websocket_handshake method" do
213
+ ws_handler = Class.new( OneShotHandler ) do
214
+ def handle_websocket_handshake( handshake )
215
+ return handshake.response
216
+ end
217
+ end
218
+
219
+ req = make_websocket_handshake()
220
+ @request_sock.should_receive( :recv ).and_return( req )
221
+
222
+ res = ws_handler.new( TEST_UUID, TEST_SEND_SPEC, TEST_RECV_SPEC ).run
223
+
224
+ res.transactions.should have( 1 ).member
225
+ request, response = res.transactions.first
226
+ request.should be_a( Mongrel2::WebSocket::ClientHandshake )
227
+ response.should be_a( Mongrel2::WebSocket::ServerHandshake )
228
+ end
229
+
230
+ it "dispatches WebSocket protocol frames to the #handle_websocket method" do
231
+ ws_handler = Class.new( OneShotHandler ) do
232
+ def handle_websocket( frame )
233
+ return frame.response
234
+ end
235
+ end
236
+
237
+ req = make_websocket_frame()
238
+ @request_sock.should_receive( :recv ).and_return( req )
239
+
240
+ res = ws_handler.new( TEST_UUID, TEST_SEND_SPEC, TEST_RECV_SPEC ).run
241
+
242
+ res.transactions.should have( 1 ).member
243
+ request, response = res.transactions.first
244
+ request.should be_a( Mongrel2::WebSocket::Frame )
245
+ response.should be_a( Mongrel2::WebSocket::Frame )
246
+ end
247
+
212
248
  it "continues when a ZMQ::Error is received but the connection remains open" do
213
249
  req = make_request()
214
250
 
@@ -55,7 +55,7 @@ describe Mongrel2::HTTPResponse do
55
55
  it "returns an empty response if its status is set to NO_CONTENT" do
56
56
  @response.puts "The response body"
57
57
  @response.status = HTTP::NO_CONTENT
58
- @response.header_data.should =~ /Content-length: 0/i
58
+ @response.header_data.should_not =~ /Content-length/i
59
59
  @response.header_data.should_not =~ /Content-type/i
60
60
  @response.to_s.should_not =~ /The response body/i
61
61
  end
@@ -69,9 +69,10 @@ describe Mongrel2::HTTPResponse do
69
69
  end
70
70
 
71
71
  it "re-calculates the automatically-added headers when re-rendered" do
72
- @response.header_data.should =~ /Content-length: 0/i
72
+ @response.header_data.should =~ /Content-length: 0\b/i
73
+ @response.status = HTTP::OK
73
74
  @response << "More data!"
74
- @response.header_data.should =~ /Content-length: 10/i
75
+ @response.header_data.should =~ /Content-length: 10\b/i
75
76
  end
76
77
 
77
78
  it "doesn't have a body" do
@@ -53,6 +53,69 @@ describe Mongrel2::WebSocket do
53
53
  BINARY_DATA.force_encoding( Encoding::ASCII_8BIT )
54
54
 
55
55
 
56
+ describe 'ClientHandshake' do
57
+
58
+ it "is the registered request type for WEBSOCKET_HANDSHAKE requests" do
59
+ Mongrel2::Request.request_types[:WEBSOCKET_HANDSHAKE].should == Mongrel2::WebSocket::ClientHandshake
60
+ end
61
+
62
+ it "knows what subprotocols were requested" do
63
+ handshake = @factory.handshake( '/websock', 'echo', 'superecho' )
64
+ handshake.protocols.should == ['echo', 'superecho']
65
+ end
66
+
67
+ it "doesn't error if no subprotocols were requested" do
68
+ handshake = @factory.handshake( '/websock' )
69
+ handshake.protocols.should == []
70
+ end
71
+
72
+ it "can create a response WebSocket::ServerHandshake for itself" do
73
+ handshake = @factory.handshake( '/websock' )
74
+ result = handshake.response
75
+ handshake.body.rewind
76
+
77
+ result.should be_a( Mongrel2::WebSocket::ServerHandshake )
78
+ result.sender_id.should == handshake.sender_id
79
+ result.conn_id.should == handshake.conn_id
80
+ result.header.sec_websocket_accept.should == handshake.body.string
81
+ result.status.should == HTTP::SWITCHING_PROTOCOLS
82
+ result.header.connection.should =~ /upgrade/i
83
+ result.header.upgrade.should =~ /websocket/i
84
+
85
+ result.body.rewind
86
+ result.body.read.should == ''
87
+ end
88
+
89
+ it "can create a response WebSocket::ServerHandshake with a valid sub-protocol for itself" do
90
+ handshake = @factory.handshake( '/websock', 'echo', 'superecho' )
91
+ result = handshake.response( :superecho )
92
+ handshake.body.rewind
93
+
94
+ result.should be_a( Mongrel2::WebSocket::ServerHandshake )
95
+ result.sender_id.should == handshake.sender_id
96
+ result.conn_id.should == handshake.conn_id
97
+ result.header.sec_websocket_accept.should == handshake.body.string
98
+ result.status.should == HTTP::SWITCHING_PROTOCOLS
99
+ result.header.connection.should =~ /upgrade/i
100
+ result.header.upgrade.should =~ /websocket/i
101
+ result.protocols.should == ['superecho']
102
+
103
+ result.body.rewind
104
+ result.body.read.should == ''
105
+ end
106
+
107
+ it "raises an exception if the specified protocol is not one of the client's advertised ones" do
108
+ handshake = @factory.handshake( '/websock', 'echo', 'superecho' )
109
+
110
+ expect {
111
+ handshake.response( :map_updates )
112
+ }.to raise_error( Mongrel2::WebSocket::HandshakeError, /map_updates/i )
113
+ end
114
+
115
+ end
116
+
117
+
118
+
56
119
  describe 'Frame' do
57
120
 
58
121
  it "is the registered request type for WEBSOCKET requests" do
@@ -310,5 +373,6 @@ describe Mongrel2::WebSocket do
310
373
 
311
374
  end
312
375
 
376
+
313
377
  end
314
378
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongrel2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -36,7 +36,7 @@ cert_chain:
36
36
  YUhDS0xaZFNLai9SSHVUT3QrZ2JsUmV4OEZBaDhOZUEKY21saFhlNDZwWk5K
37
37
  Z1dLYnhaYWg4NWpJang5NWhSOHZPSStOQU01aUg5a09xSzEzRHJ4YWNUS1Bo
38
38
  cWo1UGp3RgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
39
- date: 2012-06-26 00:00:00.000000000 Z
39
+ date: 2012-07-02 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: nokogiri
@@ -328,6 +328,7 @@ files:
328
328
  - Rakefile
329
329
  - bin/m2sh.rb
330
330
  - data/mongrel2/bootstrap.html
331
+ - data/mongrel2/config.rb.in
331
332
  - data/mongrel2/config.sql
332
333
  - data/mongrel2/css/master.css
333
334
  - data/mongrel2/js/websock-test.js
metadata.gz.sig CHANGED
Binary file