lack 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/rackup +5 -0
- data/lib/rack.rb +26 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +166 -0
- data/lib/rack/handler.rb +63 -0
- data/lib/rack/handler/webrick.rb +120 -0
- data/lib/rack/mime.rb +661 -0
- data/lib/rack/mock.rb +198 -0
- data/lib/rack/multipart.rb +31 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +239 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/request.rb +394 -0
- data/lib/rack/response.rb +160 -0
- data/lib/rack/server.rb +258 -0
- data/lib/rack/server/options.rb +121 -0
- data/lib/rack/utils.rb +653 -0
- data/lib/rack/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +5 -0
- data/test/builder/line.ru +1 -0
- data/test/builder/options.ru +2 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/fail_16384_nofile +814 -0
- data/test/multipart/file1.txt +1 -0
- data/test/multipart/filename_and_modification_param +7 -0
- data/test/multipart/filename_and_no_name +6 -0
- data/test/multipart/filename_with_escaped_quotes +6 -0
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
- data/test/multipart/filename_with_percent_escaped_quotes +6 -0
- data/test/multipart/filename_with_unescaped_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/invalid_character +6 -0
- data/test/multipart/mixed_files +21 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/semicolon +6 -0
- data/test/multipart/text +15 -0
- data/test/multipart/webkit +32 -0
- data/test/rackup/config.ru +31 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_body_proxy.rb +69 -0
- data/test/spec_builder.rb +223 -0
- data/test/spec_chunked.rb +101 -0
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +59 -0
- data/test/spec_head.rb +45 -0
- data/test/spec_lint.rb +522 -0
- data/test/spec_mime.rb +51 -0
- data/test/spec_mock.rb +277 -0
- data/test/spec_multipart.rb +547 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1199 -0
- data/test/spec_response.rb +343 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_utils.rb +635 -0
- data/test/spec_webrick.rb +184 -0
- data/test/testrequest.rb +78 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- 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
|
data/lib/rack/request.rb
ADDED
@@ -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
|