mongrel2 0.24.0 → 0.25.0.pre.285

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,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.