lack 2.0.0

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