mongrel2 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
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