lack 2.0.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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rackup +5 -0
  3. data/lib/rack.rb +26 -0
  4. data/lib/rack/body_proxy.rb +39 -0
  5. data/lib/rack/builder.rb +166 -0
  6. data/lib/rack/handler.rb +63 -0
  7. data/lib/rack/handler/webrick.rb +120 -0
  8. data/lib/rack/mime.rb +661 -0
  9. data/lib/rack/mock.rb +198 -0
  10. data/lib/rack/multipart.rb +31 -0
  11. data/lib/rack/multipart/generator.rb +93 -0
  12. data/lib/rack/multipart/parser.rb +239 -0
  13. data/lib/rack/multipart/uploaded_file.rb +34 -0
  14. data/lib/rack/request.rb +394 -0
  15. data/lib/rack/response.rb +160 -0
  16. data/lib/rack/server.rb +258 -0
  17. data/lib/rack/server/options.rb +121 -0
  18. data/lib/rack/utils.rb +653 -0
  19. data/lib/rack/version.rb +3 -0
  20. data/spec/spec_helper.rb +1 -0
  21. data/test/builder/anything.rb +5 -0
  22. data/test/builder/comment.ru +4 -0
  23. data/test/builder/end.ru +5 -0
  24. data/test/builder/line.ru +1 -0
  25. data/test/builder/options.ru +2 -0
  26. data/test/multipart/bad_robots +259 -0
  27. data/test/multipart/binary +0 -0
  28. data/test/multipart/content_type_and_no_filename +6 -0
  29. data/test/multipart/empty +10 -0
  30. data/test/multipart/fail_16384_nofile +814 -0
  31. data/test/multipart/file1.txt +1 -0
  32. data/test/multipart/filename_and_modification_param +7 -0
  33. data/test/multipart/filename_and_no_name +6 -0
  34. data/test/multipart/filename_with_escaped_quotes +6 -0
  35. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  36. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  37. data/test/multipart/filename_with_unescaped_percentages +6 -0
  38. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  39. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  40. data/test/multipart/filename_with_unescaped_quotes +6 -0
  41. data/test/multipart/ie +6 -0
  42. data/test/multipart/invalid_character +6 -0
  43. data/test/multipart/mixed_files +21 -0
  44. data/test/multipart/nested +10 -0
  45. data/test/multipart/none +9 -0
  46. data/test/multipart/semicolon +6 -0
  47. data/test/multipart/text +15 -0
  48. data/test/multipart/webkit +32 -0
  49. data/test/rackup/config.ru +31 -0
  50. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  51. data/test/spec_body_proxy.rb +69 -0
  52. data/test/spec_builder.rb +223 -0
  53. data/test/spec_chunked.rb +101 -0
  54. data/test/spec_file.rb +221 -0
  55. data/test/spec_handler.rb +59 -0
  56. data/test/spec_head.rb +45 -0
  57. data/test/spec_lint.rb +522 -0
  58. data/test/spec_mime.rb +51 -0
  59. data/test/spec_mock.rb +277 -0
  60. data/test/spec_multipart.rb +547 -0
  61. data/test/spec_recursive.rb +72 -0
  62. data/test/spec_request.rb +1199 -0
  63. data/test/spec_response.rb +343 -0
  64. data/test/spec_rewindable_input.rb +118 -0
  65. data/test/spec_sendfile.rb +130 -0
  66. data/test/spec_server.rb +167 -0
  67. data/test/spec_utils.rb +635 -0
  68. data/test/spec_webrick.rb +184 -0
  69. data/test/testrequest.rb +78 -0
  70. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  71. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  72. metadata +240 -0
@@ -0,0 +1,34 @@
1
+ module Rack
2
+ module Multipart
3
+ class UploadedFile
4
+ # The filename, *not* including the path, of the "uploaded" file
5
+ attr_reader :original_filename
6
+
7
+ # The content type of the "uploaded" file
8
+ attr_accessor :content_type
9
+
10
+ def initialize(path, content_type = "text/plain", binary = false)
11
+ raise "#{path} file does not exist" unless ::File.exist?(path)
12
+ @content_type = content_type
13
+ @original_filename = ::File.basename(path)
14
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)])
15
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
16
+ @tempfile.binmode if binary
17
+ FileUtils.copy_file(path, @tempfile.path)
18
+ end
19
+
20
+ def path
21
+ @tempfile.path
22
+ end
23
+ alias_method :local_path, :path
24
+
25
+ def respond_to?(*args)
26
+ super or @tempfile.respond_to?(*args)
27
+ end
28
+
29
+ def method_missing(method_name, *args, &block) #:nodoc:
30
+ @tempfile.__send__(method_name, *args, &block)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,394 @@
1
+ module Rack
2
+ # Rack::Request provides a convenient interface to a Rack
3
+ # environment. It is stateless, the environment +env+ passed to the
4
+ # constructor will be directly modified.
5
+ #
6
+ # req = Rack::Request.new(env)
7
+ # req.post?
8
+ # req.params["data"]
9
+
10
+ class Request
11
+ # The environment of the request.
12
+ attr_reader :env
13
+
14
+ def initialize(env)
15
+ @env = env
16
+ end
17
+
18
+ def body; @env["rack.input"] end
19
+ def script_name; @env["SCRIPT_NAME"].to_s end
20
+ def path_info; @env["PATH_INFO"].to_s end
21
+ def request_method; @env["REQUEST_METHOD"] end
22
+ def query_string; @env["QUERY_STRING"].to_s end
23
+ def content_length; @env['CONTENT_LENGTH'] end
24
+
25
+ def content_type
26
+ content_type = @env['CONTENT_TYPE']
27
+ content_type.nil? || content_type.empty? ? nil : content_type
28
+ end
29
+
30
+ def session; @env['rack.session'] ||= {} end
31
+ def session_options; @env['rack.session.options'] ||= {} end
32
+ def logger; @env['rack.logger'] end
33
+
34
+ # The media type (type/subtype) portion of the CONTENT_TYPE header
35
+ # without any media type parameters. e.g., when CONTENT_TYPE is
36
+ # "text/plain;charset=utf-8", the media-type is "text/plain".
37
+ #
38
+ # For more information on the use of media types in HTTP, see:
39
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
40
+ def media_type
41
+ content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
42
+ end
43
+
44
+ # The media type parameters provided in CONTENT_TYPE as a Hash, or
45
+ # an empty Hash if no CONTENT_TYPE or media-type parameters were
46
+ # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
47
+ # this method responds with the following Hash:
48
+ # { 'charset' => 'utf-8' }
49
+ def media_type_params
50
+ return {} if content_type.nil?
51
+ Hash[*content_type.split(/\s*[;,]\s*/)[1..-1].
52
+ collect { |s| s.split('=', 2) }.
53
+ map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten]
54
+ end
55
+
56
+ # The character set of the request body if a "charset" media type
57
+ # parameter was given, or nil if no "charset" was specified. Note
58
+ # that, per RFC2616, text/* media types that specify no explicit
59
+ # charset are to be considered ISO-8859-1.
60
+ def content_charset
61
+ media_type_params['charset']
62
+ end
63
+
64
+ def scheme
65
+ if @env['HTTPS'] == 'on'
66
+ 'https'
67
+ elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
68
+ 'https'
69
+ elsif @env['HTTP_X_FORWARDED_SCHEME']
70
+ @env['HTTP_X_FORWARDED_SCHEME']
71
+ elsif @env['HTTP_X_FORWARDED_PROTO']
72
+ @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
73
+ else
74
+ @env["rack.url_scheme"]
75
+ end
76
+ end
77
+
78
+ def ssl?
79
+ scheme == 'https'
80
+ end
81
+
82
+ def host_with_port
83
+ if forwarded = @env["HTTP_X_FORWARDED_HOST"]
84
+ forwarded.split(/,\s?/).last
85
+ else
86
+ @env['HTTP_HOST'] || "#{@env['SERVER_NAME'] || @env['SERVER_ADDR']}:#{@env['SERVER_PORT']}"
87
+ end
88
+ end
89
+
90
+ def port
91
+ if port = host_with_port.split(/:/)[1]
92
+ port.to_i
93
+ elsif port = @env['HTTP_X_FORWARDED_PORT']
94
+ port.to_i
95
+ elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
96
+ DEFAULT_PORTS[scheme]
97
+ elsif @env.has_key?("HTTP_X_FORWARDED_PROTO")
98
+ DEFAULT_PORTS[@env['HTTP_X_FORWARDED_PROTO'].split(',')[0]]
99
+ else
100
+ @env["SERVER_PORT"].to_i
101
+ end
102
+ end
103
+
104
+ def host
105
+ # Remove port number.
106
+ host_with_port.to_s.sub(/:\d+\z/, '')
107
+ end
108
+
109
+ def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end
110
+ def path_info=(s); @env["PATH_INFO"] = s.to_s end
111
+
112
+
113
+ # Checks the HTTP request method (or verb) to see if it was of type DELETE
114
+ def delete?; request_method == "DELETE" end
115
+
116
+ # Checks the HTTP request method (or verb) to see if it was of type GET
117
+ def get?; request_method == "GET" end
118
+
119
+ # Checks the HTTP request method (or verb) to see if it was of type HEAD
120
+ def head?; request_method == "HEAD" end
121
+
122
+ # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
123
+ def options?; request_method == "OPTIONS" end
124
+
125
+ # Checks the HTTP request method (or verb) to see if it was of type LINK
126
+ def link?; request_method == "LINK" end
127
+
128
+ # Checks the HTTP request method (or verb) to see if it was of type PATCH
129
+ def patch?; request_method == "PATCH" end
130
+
131
+ # Checks the HTTP request method (or verb) to see if it was of type POST
132
+ def post?; request_method == "POST" end
133
+
134
+ # Checks the HTTP request method (or verb) to see if it was of type PUT
135
+ def put?; request_method == "PUT" end
136
+
137
+ # Checks the HTTP request method (or verb) to see if it was of type TRACE
138
+ def trace?; request_method == "TRACE" end
139
+
140
+ # Checks the HTTP request method (or verb) to see if it was of type UNLINK
141
+ def unlink?; request_method == "UNLINK" end
142
+
143
+
144
+ # The set of form-data media-types. Requests that do not indicate
145
+ # one of the media types presents in this list will not be eligible
146
+ # for form-data / param parsing.
147
+ FORM_DATA_MEDIA_TYPES = [
148
+ 'application/x-www-form-urlencoded',
149
+ 'multipart/form-data'
150
+ ]
151
+
152
+ # The set of media-types. Requests that do not indicate
153
+ # one of the media types presents in this list will not be eligible
154
+ # for param parsing like soap attachments or generic multiparts
155
+ PARSEABLE_DATA_MEDIA_TYPES = [
156
+ 'multipart/related',
157
+ 'multipart/mixed'
158
+ ]
159
+
160
+ # Default ports depending on scheme. Used to decide whether or not
161
+ # to include the port in a generated URI.
162
+ DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
163
+
164
+ # Determine whether the request body contains form-data by checking
165
+ # the request Content-Type for one of the media-types:
166
+ # "application/x-www-form-urlencoded" or "multipart/form-data". The
167
+ # list of form-data media types can be modified through the
168
+ # +FORM_DATA_MEDIA_TYPES+ array.
169
+ #
170
+ # A request body is also assumed to contain form-data when no
171
+ # Content-Type header is provided and the request_method is POST.
172
+ def form_data?
173
+ type = media_type
174
+ meth = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
175
+ (meth == 'POST' && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
176
+ end
177
+
178
+ # Determine whether the request body contains data by checking
179
+ # the request media_type against registered parse-data media-types
180
+ def parseable_data?
181
+ PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
182
+ end
183
+
184
+ # Returns the data received in the query string.
185
+ def GET
186
+ if @env["rack.request.query_string"] == query_string
187
+ @env["rack.request.query_hash"]
188
+ else
189
+ p = parse_query(query_string)
190
+ @env["rack.request.query_string"] = query_string
191
+ @env["rack.request.query_hash"] = p
192
+ end
193
+ end
194
+
195
+ # Returns the data received in the request body.
196
+ #
197
+ # This method support both application/x-www-form-urlencoded and
198
+ # multipart/form-data.
199
+ def POST
200
+ if @env["rack.input"].nil?
201
+ raise "Missing rack.input"
202
+ elsif @env["rack.request.form_input"].equal? @env["rack.input"]
203
+ @env["rack.request.form_hash"]
204
+ elsif form_data? || parseable_data?
205
+ unless @env["rack.request.form_hash"] = parse_multipart(env)
206
+ form_vars = @env["rack.input"].read
207
+
208
+ # Fix for Safari Ajax postings that always append \0
209
+ # form_vars.sub!(/\0\z/, '') # performance replacement:
210
+ form_vars.slice!(-1) if form_vars[-1] == ?\0
211
+
212
+ @env["rack.request.form_vars"] = form_vars
213
+ @env["rack.request.form_hash"] = parse_query(form_vars)
214
+
215
+ @env["rack.input"].rewind
216
+ end
217
+ @env["rack.request.form_input"] = @env["rack.input"]
218
+ @env["rack.request.form_hash"]
219
+ else
220
+ {}
221
+ end
222
+ end
223
+
224
+ # The union of GET and POST data.
225
+ #
226
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
227
+ def params
228
+ @params ||= self.GET.merge(self.POST)
229
+ rescue EOFError
230
+ self.GET.dup
231
+ end
232
+
233
+ # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
234
+ #
235
+ # The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
236
+ #
237
+ # env['rack.input'] is not touched.
238
+ def update_param(k, v)
239
+ found = false
240
+ if self.GET.has_key?(k)
241
+ found = true
242
+ self.GET[k] = v
243
+ end
244
+ if self.POST.has_key?(k)
245
+ found = true
246
+ self.POST[k] = v
247
+ end
248
+ unless found
249
+ self.GET[k] = v
250
+ end
251
+ @params = nil
252
+ nil
253
+ end
254
+
255
+ # Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
256
+ #
257
+ # If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
258
+ #
259
+ # env['rack.input'] is not touched.
260
+ def delete_param(k)
261
+ v = [ self.POST.delete(k), self.GET.delete(k) ].compact.first
262
+ @params = nil
263
+ v
264
+ end
265
+
266
+ # shortcut for request.params[key]
267
+ def [](key)
268
+ params[key.to_s]
269
+ end
270
+
271
+ # shortcut for request.params[key] = value
272
+ #
273
+ # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
274
+ def []=(key, value)
275
+ params[key.to_s] = value
276
+ end
277
+
278
+ # like Hash#values_at
279
+ def values_at(*keys)
280
+ keys.map{|key| params[key] }
281
+ end
282
+
283
+ # the referer of the client
284
+ def referer
285
+ @env['HTTP_REFERER']
286
+ end
287
+ alias referrer referer
288
+
289
+ def user_agent
290
+ @env['HTTP_USER_AGENT']
291
+ end
292
+
293
+ def cookies
294
+ hash = @env["rack.request.cookie_hash"] ||= {}
295
+ string = @env["HTTP_COOKIE"]
296
+
297
+ return hash if string == @env["rack.request.cookie_string"]
298
+ hash.clear
299
+
300
+ # According to RFC 2109:
301
+ # If multiple cookies satisfy the criteria above, they are ordered in
302
+ # the Cookie header such that those with more specific Path attributes
303
+ # precede those with less specific. Ordering with respect to other
304
+ # attributes (e.g., Domain) is unspecified.
305
+ cookies = Utils.parse_query(string, ';,') { |s| Rack::Utils.unescape(s) rescue s }
306
+ cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
307
+ @env["rack.request.cookie_string"] = string
308
+ hash
309
+ end
310
+
311
+ def xhr?
312
+ @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
313
+ end
314
+
315
+ def base_url
316
+ url = "#{scheme}://#{host}"
317
+ url << ":#{port}" if port != DEFAULT_PORTS[scheme]
318
+ url
319
+ end
320
+
321
+ # Tries to return a remake of the original request URL as a string.
322
+ def url
323
+ base_url + fullpath
324
+ end
325
+
326
+ def path
327
+ script_name + path_info
328
+ end
329
+
330
+ def fullpath
331
+ query_string.empty? ? path : "#{path}?#{query_string}"
332
+ end
333
+
334
+ def accept_encoding
335
+ parse_http_accept_header(@env["HTTP_ACCEPT_ENCODING"])
336
+ end
337
+
338
+ def accept_language
339
+ parse_http_accept_header(@env["HTTP_ACCEPT_LANGUAGE"])
340
+ end
341
+
342
+ def trusted_proxy?(ip)
343
+ ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
344
+ end
345
+
346
+ def ip
347
+ remote_addrs = split_ip_addresses(@env['REMOTE_ADDR'])
348
+ remote_addrs = reject_trusted_ip_addresses(remote_addrs)
349
+
350
+ return remote_addrs.first if remote_addrs.any?
351
+
352
+ forwarded_ips = split_ip_addresses(@env['HTTP_X_FORWARDED_FOR'])
353
+
354
+ return reject_trusted_ip_addresses(forwarded_ips).last || @env["REMOTE_ADDR"]
355
+ end
356
+
357
+
358
+ protected def split_ip_addresses(ip_addresses)
359
+ ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
360
+ end
361
+
362
+ protected def reject_trusted_ip_addresses(ip_addresses)
363
+ ip_addresses.reject { |ip| trusted_proxy?(ip) }
364
+ end
365
+
366
+ protected def parse_query(qs)
367
+ Utils.parse_nested_query(qs, '&')
368
+ end
369
+
370
+ protected def parse_multipart(env)
371
+ Rack::Multipart.parse_multipart(env)
372
+ end
373
+
374
+ protected def parse_http_accept_header(header)
375
+ header.to_s.split(/\s*,\s*/).map do |part|
376
+ attribute, parameters = part.split(/\s*;\s*/, 2)
377
+ quality = 1.0
378
+ if parameters and /\Aq=([\d.]+)/ =~ parameters
379
+ quality = $1.to_f
380
+ end
381
+ [attribute, quality]
382
+ end
383
+ end
384
+
385
+
386
+ private def strip_doublequotes(s)
387
+ if s[0] == ?" && s[-1] == ?"
388
+ s[1..-2]
389
+ else
390
+ s
391
+ end
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,160 @@
1
+ require 'rack/request'
2
+ require 'rack/utils'
3
+ require 'rack/body_proxy'
4
+ require 'time'
5
+
6
+ module Rack
7
+ # Rack::Response provides a convenient interface to create a Rack
8
+ # response.
9
+ #
10
+ # It allows setting of headers and cookies, and provides useful
11
+ # defaults (a OK response containing HTML).
12
+ #
13
+ # You can use Response#write to iteratively generate your response,
14
+ # but note that this is buffered by Rack::Response until you call
15
+ # +finish+. +finish+ however can take a block inside which calls to
16
+ # +write+ are synchronous with the Rack response.
17
+ #
18
+ # Your application's +call+ should end returning Response#finish.
19
+
20
+ class Response
21
+ attr_accessor :length
22
+
23
+ def initialize(body=[], status=200, header={})
24
+ @status = status.to_i
25
+ @header = Utils::HeaderHash.new.merge(header)
26
+
27
+ @chunked = "chunked" == @header['Transfer-Encoding']
28
+ @writer = lambda { |x| @body << x }
29
+ @block = nil
30
+ @length = 0
31
+
32
+ @body = []
33
+
34
+ if body.respond_to? :to_str
35
+ write body.to_str
36
+ elsif body.respond_to?(:each)
37
+ body.each { |part|
38
+ write part.to_s
39
+ }
40
+ else
41
+ raise TypeError, "stringable or iterable required"
42
+ end
43
+
44
+ yield self if block_given?
45
+ end
46
+
47
+ attr_reader :header
48
+ attr_accessor :status, :body
49
+
50
+ def [](key)
51
+ header[key]
52
+ end
53
+
54
+ def []=(key, value)
55
+ header[key] = value
56
+ end
57
+
58
+ def set_cookie(key, value)
59
+ Utils.set_cookie_header!(header, key, value)
60
+ end
61
+
62
+ def delete_cookie(key, value={})
63
+ Utils.delete_cookie_header!(header, key, value)
64
+ end
65
+
66
+ def redirect(target, status=302)
67
+ self.status = status
68
+ self["Location"] = target
69
+ end
70
+
71
+ def finish(&block)
72
+ @block = block
73
+
74
+ if [204, 205, 304].include?(status.to_i)
75
+ header.delete "Content-Type"
76
+ header.delete "Content-Length"
77
+ close
78
+ [status.to_i, header, []]
79
+ else
80
+ [status.to_i, header, BodyProxy.new(self){}]
81
+ end
82
+ end
83
+ alias to_a finish # For *response
84
+ alias to_ary finish # For implicit-splat on Ruby 1.9.2
85
+
86
+ def each(&callback)
87
+ @body.each(&callback)
88
+ @writer = callback
89
+ @block.call(self) if @block
90
+ end
91
+
92
+ # Append to body and update Content-Length.
93
+ #
94
+ # NOTE: Do not mix #write and direct #body access!
95
+ #
96
+ def write(str)
97
+ s = str.to_s
98
+ @length += Rack::Utils.bytesize(s) unless @chunked
99
+ @writer.call s
100
+
101
+ header["Content-Length"] = @length.to_s unless @chunked
102
+ str
103
+ end
104
+
105
+ def close
106
+ body.close if body.respond_to?(:close)
107
+ end
108
+
109
+ def empty?
110
+ @block == nil && @body.empty?
111
+ end
112
+
113
+ alias headers header
114
+
115
+ module Helpers
116
+ def invalid?; status < 100 || status >= 600; end
117
+
118
+ def informational?; status >= 100 && status < 200; end
119
+ def successful?; status >= 200 && status < 300; end
120
+ def redirection?; status >= 300 && status < 400; end
121
+ def client_error?; status >= 400 && status < 500; end
122
+ def server_error?; status >= 500 && status < 600; end
123
+
124
+ def ok?; status == 200; end
125
+ def created?; status == 201; end
126
+ def accepted?; status == 202; end
127
+ def bad_request?; status == 400; end
128
+ def unauthorized?; status == 401; end
129
+ def forbidden?; status == 403; end
130
+ def not_found?; status == 404; end
131
+ def method_not_allowed?; status == 405; end
132
+ def i_m_a_teapot?; status == 418; end
133
+ def unprocessable?; status == 422; end
134
+
135
+ def redirect?; [301, 302, 303, 307].include? status; end
136
+
137
+ # Headers
138
+ attr_reader :headers, :original_headers
139
+
140
+ def include?(header)
141
+ !!headers[header]
142
+ end
143
+
144
+ def content_type
145
+ headers["Content-Type"]
146
+ end
147
+
148
+ def content_length
149
+ cl = headers["Content-Length"]
150
+ cl ? cl.to_i : cl
151
+ end
152
+
153
+ def location
154
+ headers["Location"]
155
+ end
156
+ end
157
+
158
+ include Helpers
159
+ end
160
+ end