mongrel2 0.24.0 → 0.25.0.pre.285

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,9 +1,61 @@
1
+ 2012-06-20 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ * examples/async-upload.rb, examples/config.rb,
4
+ lib/mongrel2/httprequest.rb, lib/mongrel2/request.rb,
5
+ lib/mongrel2/testing.rb, spec/mongrel2/httprequest_spec.rb,
6
+ spec/mongrel2/request_spec.rb:
7
+ Hook up the async uploaded entity body to the request
8
+ [349c0049a4a1] [tip]
9
+
10
+ 2012-06-19 Michael Granger <ged@FaerieMUD.org>
11
+
12
+ * examples/Procfile, examples/async-upload.rb, examples/config.rb,
13
+ examples/ws-echo.rb, lib/mongrel2/websocket.rb,
14
+ spec/mongrel2/websocket_spec.rb:
15
+ Fixes for the websocket frame class for the stream API, fixed some
16
+ examples.
17
+ [4202d9942c16]
18
+
19
+ * lib/mongrel2/connection.rb, lib/mongrel2/httpresponse.rb,
20
+ lib/mongrel2/request.rb, lib/mongrel2/response.rb,
21
+ lib/mongrel2/websocket.rb, spec/mongrel2/httpresponse_spec.rb,
22
+ spec/mongrel2/response_spec.rb, spec/mongrel2/websocket_spec.rb:
23
+ Convert to an IO-based request/response body
24
+ [96edda5cdb69]
25
+
26
+ 2012-06-08 Mahlon E. Smith <mahlon@martini.nu>
27
+
28
+ * lib/mongrel2/constants.rb:
29
+ Add a few newer-rfc HTTP error codes.
30
+ [8e125eaf25ce]
31
+
32
+ 2012-05-31 Michael Granger <ged@FaerieMUD.org>
33
+
34
+ * .hgtags:
35
+ Added tag v0.24.0 for changeset d2b1df5fc74d
36
+ [dbe3504c63ad]
37
+
38
+ * .hgsigs:
39
+ Added signature for changeset 3ed1175a2f9e
40
+ [d2b1df5fc74d] [v0.24.0]
41
+
42
+ * History.rdoc, lib/mongrel2.rb:
43
+ Bumping minor version, updating history.
44
+ [3ed1175a2f9e]
45
+
46
+ 2012-05-29 Mahlon E. Smith <mahlon@martini.nu>
47
+
48
+ * lib/mongrel2/table.rb:
49
+ Take immediate objects into account when dup/cloning a
50
+ Mongrel2::Table.
51
+ [f10cba1487ea]
52
+
1
53
  2012-05-20 Michael Granger <ged@FaerieMUD.org>
2
54
 
3
55
  * .rvm.gems:
4
56
  Comment out amalgalite rvmrc gem until
5
57
  https://github.com/copiousfreetime/amalgalite/pull/22 is fixed
6
- [b872e092ed92] [tip]
58
+ [b872e092ed92]
7
59
 
8
60
  2012-05-21 Michael Granger <ged@FaerieMUD.org>
9
61
 
data/History.rdoc CHANGED
@@ -1,3 +1,17 @@
1
+ == v0.25.0 [2012-06-20] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ NOTE: This revision contains non-backward-compatible changes to
4
+ Mongrel2::Request, Mongrel2::Response, and all subclasses.
5
+
6
+ - Convert request and response entity bodies to IOish objects instead of
7
+ Strings.
8
+ - Finished implementation of Mongrel2 async upload API -- async-uploaded
9
+ entity bodies now become File entity bodies of the
10
+ "X-Mongrel2-Upload-Done" request.
11
+ - Add a few newer-rfc HTTP error codes.
12
+ - Add support for Content-type charsets.
13
+
14
+
1
15
  == v0.24.0 [2012-05-31] Michael Granger <ged@FaerieMUD.org>
2
16
 
3
17
  - Fix a bug when duping a Mongrel2::Table with immediate objects as values.
data/examples/Procfile CHANGED
@@ -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
-
6
+ ws: ruby ws-echo.rb
@@ -8,6 +8,9 @@ require 'mongrel2/handler'
8
8
  # A example of how to allow Mongrel2's async uploads.
9
9
  class AsyncUploadHandler < Mongrel2::Handler
10
10
 
11
+ # App ID
12
+ ID = 'async-upload'
13
+
11
14
  ### Load up the ERB template from the DATA section on instantiation.
12
15
  def initialize( * )
13
16
  super
@@ -35,7 +38,7 @@ class AsyncUploadHandler < Mongrel2::Handler
35
38
  [ request.uploaded_file, request.content_length, request.content_type ]
36
39
 
37
40
  response = request.response
38
- response.puts "Upload complete: %s" % [ request.uploaded_file ]
41
+ response.puts "Upload complete: %p" % [ request.uploaded_file ]
39
42
  response.content_type = 'text/plain'
40
43
 
41
44
  return response
data/examples/config.rb CHANGED
@@ -45,9 +45,10 @@ server 'examples' do
45
45
 
46
46
  end
47
47
 
48
- setting "zeromq.threads", 1
49
- setting "limits.content_length", 4096
50
- setting "upload.temp_store", upload_dir + 'mongrel2.upload.XXXXXX'
48
+ setting 'zeromq.threads', 1
49
+ setting 'limits.content_length', 4096
50
+ setting 'control_port', 'ipc://var/run/control'
51
+ setting 'upload.temp_store', upload_dir + 'mongrel2.upload.XXXXXX'
51
52
 
52
53
  mkdir_p 'var/run'
53
54
  mkdir_p 'logs'
@@ -17,6 +17,7 @@ class RequestDumper < Mongrel2::Handler
17
17
  def initialize( * )
18
18
  super
19
19
  @template = Inversion::Template.load( 'request-dumper.tmpl' )
20
+ $SAFE = 1
20
21
  end
21
22
 
22
23
 
@@ -20,7 +20,7 @@
20
20
  </header>
21
21
 
22
22
  <section id="dump">
23
-
23
+
24
24
  <table>
25
25
  <tr>
26
26
  <th>Sender ID</th>
@@ -35,7 +35,7 @@
35
35
  <td><?attr safelevel ?></td>
36
36
  </tr>
37
37
  </table>
38
-
38
+
39
39
  <section id="headers">
40
40
  <header>
41
41
  <h1>Headers</h1>
@@ -49,13 +49,14 @@
49
49
  <?end for ?>
50
50
  </table>
51
51
  </section>
52
-
52
+
53
53
  <section id="body">
54
54
  <header>
55
55
  <h1>Body</h1>
56
56
  </header>
57
57
 
58
- <p><code><?escape request.body.dump ?></code></p>
58
+ <p><code><?escape request.body.inspect ?></code>
59
+ (<?call "%0.2fKb" % request.body.size ?>, <?call request.body.external_encoding.name ?>)</p>
59
60
  </section>
60
61
 
61
62
  <section id="inspect">
@@ -66,7 +67,7 @@
66
67
  </pre>
67
68
  </header>
68
69
  </section>
69
-
70
+
70
71
  </section>
71
72
 
72
73
  <footer>
data/examples/ws-echo.rb CHANGED
@@ -4,7 +4,6 @@
4
4
  require 'loggability'
5
5
  require 'pathname'
6
6
  require 'mongrel2/config'
7
- require 'mongrel2/logging'
8
7
  require 'mongrel2/handler'
9
8
 
10
9
 
@@ -99,8 +98,9 @@ class WebSocketEchoServer < Mongrel2::Handler
99
98
  frame.opcode.to_s.upcase,
100
99
  frame.fin? ? '' : '(cont)',
101
100
  frame.headers.x_forwarded_for,
102
- frame.payload[ 0, 20 ],
101
+ frame.payload.read( 20 ),
103
102
  ]
103
+ frame.payload.rewind
104
104
 
105
105
  # If a client sends an invalid frame, close their connection, but politely.
106
106
  if !frame.valid?
@@ -130,7 +130,7 @@ class WebSocketEchoServer < Mongrel2::Handler
130
130
  # Make the response frame
131
131
  response = frame.response
132
132
  response.fin = frame.fin?
133
- response.payload = frame.payload
133
+ IO.copy_stream( frame.payload, response.payload )
134
134
 
135
135
  return response
136
136
  end
data/lib/mongrel2.rb CHANGED
@@ -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.24.0'
23
+ VERSION = '0.25.0'
24
24
 
25
25
  # Version-control revision constant
26
- REVISION = %q$Revision: 3ed1175a2f9e $
26
+ REVISION = %q$Revision: 928743f397cc $
27
27
 
28
28
 
29
29
  require 'mongrel2/constants'
@@ -105,7 +105,7 @@ class Mongrel2::Connection
105
105
 
106
106
  self.log.debug "Fetching next request (PULL)"
107
107
  data = self.request_sock.recv
108
- self.log.debug " got request data: %p" % [ data ]
108
+ self.log.debug " got %s request data: %p" % [ data.encoding.name, data ]
109
109
  return data
110
110
  end
111
111
 
@@ -131,7 +131,9 @@ class Mongrel2::Connection
131
131
 
132
132
  ### Write the specified +response+ (Mongrel::Response object) to the requester.
133
133
  def reply( response )
134
- self.send( response.sender_id, response.conn_id, response.to_s )
134
+ response.each_chunk do |data|
135
+ self.send( response.sender_id, response.conn_id, data )
136
+ end
135
137
  end
136
138
 
137
139
 
@@ -65,6 +65,10 @@ module Mongrel2::Constants
65
65
  UNPROCESSABLE_ENTITY = 422
66
66
  LOCKED = 423
67
67
  FAILED_DEPENDENCY = 424
68
+ UPGRADE_REQUIRED = 426
69
+ RECONDITION_REQUIRED = 428
70
+ TOO_MANY_REQUESTS = 429
71
+ REQUEST_HEADERS_TOO_LARGE = 431
68
72
 
69
73
  SERVER_ERROR = 500
70
74
  NOT_IMPLEMENTED = 501
@@ -140,6 +144,9 @@ module Mongrel2::Constants
140
144
  424 => "Failed Dependency",
141
145
  425 => "No code",
142
146
  426 => "Upgrade Required",
147
+ 428 => "Precondition Required",
148
+ 429 => "Too Many Requests",
149
+ 431 => "Request Headers too Large",
143
150
  500 => "Internal Server Error",
144
151
  501 => "Method Not Implemented",
145
152
  502 => "Bad Gateway",
@@ -100,48 +100,6 @@ class Mongrel2::HTTPRequest < Mongrel2::Request
100
100
  end
101
101
 
102
102
 
103
- #
104
- # :section: Async Upload Support
105
- # See http://mongrel2.org/static/book-finalch6.html#x8-810005.5 for details.
106
- #
107
-
108
- ### The Pathname, relative to Mongrel2's chroot path, of the uploaded entity body.
109
- def uploaded_file
110
- raise Mongrel2::UploadError, "invalid upload: upload headers don't match" unless
111
- self.upload_headers_match?
112
- return Pathname( self.headers.x_mongrel2_upload_done )
113
- end
114
-
115
-
116
- ### Returns +true+ if this request is an 'asynchronous upload started' notification.
117
- def upload_started?
118
- return self.headers.member?( :x_mongrel2_upload_start ) &&
119
- !self.headers.member?( :x_mongrel2_upload_done )
120
- end
121
-
122
-
123
- ### Returns +true+ if this request is an 'asynchronous upload done' notification.
124
- def upload_done?
125
- return self.headers.member?( :x_mongrel2_upload_start ) &&
126
- self.headers.member?( :x_mongrel2_upload_done )
127
- end
128
-
129
-
130
- ### Returns +true+ if this request is an 'asynchronous upload done' notification
131
- ### and the two headers match (trivial guard against forgery)
132
- def upload_headers_match?
133
- return self.upload_done? &&
134
- self.headers.x_mongrel2_upload_start == self.headers.x_mongrel2_upload_done
135
- end
136
-
137
-
138
- ### Returns true if this request is an asynchronous upload, and the filename of the
139
- ### finished request matches the one from the starting notification.
140
- def valid_upload?
141
- return self.upload_done? && self.upload_headers_match?
142
- end
143
-
144
-
145
103
  #########
146
104
  protected
147
105
  #########
@@ -37,7 +37,7 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
37
37
 
38
38
  @headers = Mongrel2::Table.new
39
39
  @status = nil
40
- self.reset
40
+ self.set_defaults
41
41
 
42
42
  @headers.merge!( headers )
43
43
  end
@@ -55,12 +55,18 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
55
55
  attr_accessor :status
56
56
 
57
57
 
58
+ ### Set up response default headers, etc.
59
+ def set_defaults
60
+ @headers[:server] = Mongrel2.version_string( true )
61
+ end
62
+
63
+
58
64
  ### Stringify the response
59
65
  def to_s
60
66
  return [
61
67
  self.status_line,
62
68
  self.header_data,
63
- self.bodiless? ? '' : self.body
69
+ self.bodiless? ? '' : super
64
70
  ].join( "\r\n" )
65
71
  end
66
72
 
@@ -69,8 +75,8 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
69
75
  def status_line
70
76
  self.log.debug "Building status line for status: %p" % [ self.status ]
71
77
 
72
- st = self.status ||
73
- ((self.body.nil? || self.body.empty?) ? HTTP::NO_CONTENT : HTTP::OK)
78
+ st = self.status || (self.body.size.zero? ? HTTP::NO_CONTENT : HTTP::OK)
79
+
74
80
  return STATUS_LINE_FORMAT % [ st, HTTP::STATUS_NAME[st] ]
75
81
  end
76
82
 
@@ -141,9 +147,10 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
141
147
  ### Clear any existing headers and body and restore them to their defaults
142
148
  def reset
143
149
  @headers.clear
144
- @headers[:server] = Mongrel2.version_string( true )
150
+ @body.truncate( 0 )
145
151
  @status = nil
146
- @body = ''
152
+
153
+ self.set_defaults
147
154
 
148
155
  return true
149
156
  end
@@ -174,25 +181,15 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
174
181
  end
175
182
 
176
183
 
177
- ### Get the length of the body, either by calling its #length method if it has
178
- ### one, or using #seek and #tell if it implements those. If neither of those are
179
- ### possible, an exception is raised.
184
+ ### Get the length of the body IO. If the IO's offset is somewhere other than
185
+ ### the beginning or end, the size of the remainder is used.
180
186
  def get_content_length
181
187
  if self.bodiless?
182
188
  return 0
183
- elsif self.body.respond_to?( :bytesize )
184
- return self.body.bytesize
185
- elsif self.body.respond_to?( :seek ) && self.body.respond_to?( :tell )
186
- starting_pos = self.body.tell
187
- self.body.seek( 0, IO::SEEK_END )
188
- length = self.body.tell - starting_pos
189
- self.body.seek( starting_pos, IO::SEEK_SET )
190
-
191
- return length
189
+ elsif self.body.pos.nonzero? && !self.body.eof?
190
+ return self.body.size - self.body.pos
192
191
  else
193
- raise Mongrel2::ResponseError,
194
- "No way to calculate the content length of the response (a %s)." %
195
- [ self.body.class.name ]
192
+ return self.body.size
196
193
  end
197
194
  end
198
195
 
@@ -219,10 +216,11 @@ class Mongrel2::HTTPResponse < Mongrel2::Response
219
216
 
220
217
  ### Return the details to include in the contents of the #inspected object.
221
218
  def inspect_details
222
- return %Q{%s -- %d headers, %0.2fK body} % [
219
+ return %Q{%s -- %d headers, %0.2fK body (%p)} % [
223
220
  self.status_line,
224
221
  self.headers.length,
225
222
  (self.get_content_length / 1024.0),
223
+ self.body,
226
224
  ]
227
225
  end
228
226
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
+ require 'stringio'
3
4
  require 'tnetstring'
4
5
  require 'yajl'
5
6
  require 'loggability'
@@ -110,13 +111,14 @@ class Mongrel2::Request
110
111
  ### and +body+. The optional +nil+ is for the raw request content, which can be useful
111
112
  ### later for debugging.
112
113
  def initialize( sender_id, conn_id, path, headers, body='', raw=nil )
114
+
113
115
  @sender_id = sender_id
114
116
  @conn_id = Integer( conn_id )
115
117
  @path = path
116
118
  @headers = Mongrel2::Table.new( headers )
117
- @body = body
118
119
  @raw = raw
119
120
 
121
+ @body = self.make_entity_body( body )
120
122
  @response = nil
121
123
  end
122
124
 
@@ -131,7 +133,7 @@ class Mongrel2::Request
131
133
  # The listener ID on the server
132
134
  attr_reader :conn_id
133
135
 
134
- # The path component of the requested URL in HTTP, or the equivalent
136
+ # The path component of the requested URL in HTTP, or the equivalent
135
137
  # for other request types
136
138
  attr_reader :path
137
139
 
@@ -139,7 +141,7 @@ class Mongrel2::Request
139
141
  attr_reader :headers
140
142
  alias_method :header, :headers
141
143
 
142
- # The request body data, if there is any, as a String
144
+ # The request body data, if there is any, as an IO object
143
145
  attr_reader :body
144
146
 
145
147
  # The raw request content, if the request was parsed from mongrel2
@@ -161,6 +163,69 @@ class Mongrel2::Request
161
163
  end
162
164
 
163
165
 
166
+ #
167
+ # :section: Async Upload Support
168
+ # See http://mongrel2.org/static/book-finalch6.html#x8-810005.5 for details.
169
+ #
170
+
171
+ ### The Pathname, relative to Mongrel2's chroot path, of the uploaded entity body.
172
+ def uploaded_file
173
+ raise Mongrel2::UploadError, "invalid upload: upload headers don't match" unless
174
+ self.upload_headers_match?
175
+
176
+ server = Mongrel2::Config::Server.by_uuid( self.sender_id ).first or
177
+ raise Mongrel2::UploadError, "couldn't find the server %p in the config DB" %
178
+ [ self.sender_id ]
179
+
180
+ relpath = Pathname( self.headers.x_mongrel2_upload_done )
181
+ chrooted = Pathname( server.chroot ) + relpath
182
+
183
+ if chrooted.exist?
184
+ return chrooted
185
+ elsif relpath.exist?
186
+ return relpath
187
+ else
188
+ self.log.error "uploaded body %s not found: tried relative to cwd and server chroot (%s)" %
189
+ [ relpath, server.chroot ]
190
+ raise Mongrel2::UploadError,
191
+ "couldn't find the path to uploaded body."
192
+ end
193
+ end
194
+
195
+
196
+ ### Returns +true+ if this request is an 'asynchronous upload started' notification.
197
+ def upload_started?
198
+ return self.headers.member?( :x_mongrel2_upload_start ) &&
199
+ !self.headers.member?( :x_mongrel2_upload_done )
200
+ end
201
+
202
+
203
+ ### Returns +true+ if this request is an 'asynchronous upload done' notification.
204
+ def upload_done?
205
+ return self.headers.member?( :x_mongrel2_upload_start ) &&
206
+ self.headers.member?( :x_mongrel2_upload_done )
207
+ end
208
+
209
+
210
+ ### Returns +true+ if this request is an 'asynchronous upload done' notification
211
+ ### and the two headers match (trivial guard against forgery)
212
+ def upload_headers_match?
213
+ return self.upload_done? &&
214
+ self.headers.x_mongrel2_upload_start == self.headers.x_mongrel2_upload_done
215
+ end
216
+
217
+
218
+ ### Returns true if this request is an asynchronous upload, and the filename of the
219
+ ### finished request matches the one from the starting notification.
220
+ def valid_upload?
221
+ return self.upload_done? && self.upload_headers_match?
222
+ end
223
+
224
+
225
+ #
226
+ # :section: Introspection Methods
227
+ #
228
+
164
229
  ### Returns a string containing a human-readable representation of the Request,
165
230
  ### suitable for debugging.
166
231
  def inspect
@@ -178,6 +243,35 @@ class Mongrel2::Request
178
243
  protected
179
244
  #########
180
245
 
246
+ ### Convert the entity +body+ into an IOish object, wrapping it in a StringIO if
247
+ ### it doesn't already respond to :read, :pos, and :seek. If the request has
248
+ ### valid 'X-Mongrel2-Upload-*' headers (the async upload API), a File object
249
+ ### opened to the spool file will be returned instead.
250
+ def make_entity_body( body )
251
+ # :TODO: Handle Content-Encoding, too.
252
+
253
+ enc = self.headers.content_type[ /\bcharset=(\S+)/, 1 ] if self.headers.content_type
254
+
255
+ if self.valid_upload?
256
+ enc ||= Encoding::ASCII_8BIT
257
+ spoolfile = self.uploaded_file
258
+ self.log.info "Using async %s spool file %s as request entity body." % [ enc, spoolfile ]
259
+ return spoolfile.open( 'r', encoding: enc )
260
+
261
+ elsif !( body.respond_to?(:read) && body.respond_to?(:pos) && body.respond_to?(:seek) )
262
+ self.log.info "Wrapping non-IO (%p) body in a StringIO" % [ body.class ]
263
+
264
+ # Get the object as a String, set the encoding
265
+ str = body.to_s
266
+ str.force_encoding( enc ) if enc && str.encoding == Encoding::ASCII_8BIT
267
+
268
+ return StringIO.new( str, 'r+' )
269
+ else
270
+ return body
271
+ end
272
+ end
273
+
274
+
181
275
  ### Return the details to include in the contents of the #inspected object. This
182
276
  ### method allows other request types to provide their own details while keeping
183
277
  ### the form somewhat consistent.