rack 1.1.6 → 1.6.9
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.
- checksums.yaml +7 -0
- data/COPYING +1 -1
- data/HISTORY.md +375 -0
- data/KNOWN-ISSUES +23 -0
- data/README.rdoc +312 -0
- data/Rakefile +124 -0
- data/SPEC +125 -32
- data/contrib/rack.png +0 -0
- data/contrib/rack.svg +150 -0
- data/contrib/rack_logo.svg +1 -1
- data/contrib/rdoc.css +412 -0
- data/example/protectedlobster.rb +1 -1
- data/lib/rack/auth/abstract/handler.rb +4 -4
- data/lib/rack/auth/abstract/request.rb +7 -5
- data/lib/rack/auth/basic.rb +1 -1
- data/lib/rack/auth/digest/md5.rb +7 -3
- data/lib/rack/auth/digest/nonce.rb +1 -1
- data/lib/rack/auth/digest/params.rb +7 -9
- data/lib/rack/auth/digest/request.rb +10 -9
- data/lib/rack/backports/uri/common_18.rb +56 -0
- data/lib/rack/backports/uri/common_192.rb +52 -0
- data/lib/rack/backports/uri/common_193.rb +29 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +106 -22
- data/lib/rack/cascade.rb +17 -6
- data/lib/rack/chunked.rb +44 -24
- data/lib/rack/commonlogger.rb +36 -13
- data/lib/rack/conditionalget.rb +49 -17
- data/lib/rack/config.rb +5 -0
- data/lib/rack/content_length.rb +14 -6
- data/lib/rack/content_type.rb +7 -1
- data/lib/rack/deflater.rb +73 -15
- data/lib/rack/directory.rb +18 -8
- data/lib/rack/etag.rb +59 -9
- data/lib/rack/file.rb +106 -44
- data/lib/rack/handler/cgi.rb +11 -11
- data/lib/rack/handler/fastcgi.rb +18 -6
- data/lib/rack/handler/lsws.rb +2 -4
- data/lib/rack/handler/mongrel.rb +22 -6
- data/lib/rack/handler/scgi.rb +16 -8
- data/lib/rack/handler/thin.rb +19 -4
- data/lib/rack/handler/webrick.rb +72 -19
- data/lib/rack/handler.rb +47 -14
- data/lib/rack/head.rb +10 -2
- data/lib/rack/lint.rb +260 -75
- data/lib/rack/lobster.rb +13 -8
- data/lib/rack/lock.rb +13 -3
- data/lib/rack/logger.rb +0 -2
- data/lib/rack/methodoverride.rb +27 -8
- data/lib/rack/mime.rb +625 -167
- data/lib/rack/mock.rb +78 -53
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +253 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/multipart.rb +34 -0
- data/lib/rack/nulllogger.rb +21 -2
- data/lib/rack/recursive.rb +10 -5
- data/lib/rack/reloader.rb +3 -2
- data/lib/rack/request.rb +201 -74
- data/lib/rack/response.rb +41 -28
- data/lib/rack/rewindable_input.rb +15 -11
- data/lib/rack/runtime.rb +16 -3
- data/lib/rack/sendfile.rb +47 -29
- data/lib/rack/server.rb +223 -47
- data/lib/rack/session/abstract/id.rb +289 -30
- data/lib/rack/session/cookie.rb +133 -44
- data/lib/rack/session/memcache.rb +30 -56
- data/lib/rack/session/pool.rb +19 -43
- data/lib/rack/showexceptions.rb +53 -15
- data/lib/rack/showstatus.rb +14 -7
- data/lib/rack/static.rb +124 -12
- data/lib/rack/tempfile_reaper.rb +22 -0
- data/lib/rack/urlmap.rb +49 -15
- data/lib/rack/utils/okjson.rb +600 -0
- data/lib/rack/utils.rb +363 -361
- data/lib/rack.rb +17 -23
- data/rack.gemspec +11 -20
- 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/cgi/assets/folder/test.js +1 -0
- data/test/cgi/assets/fonts/font.eot +1 -0
- data/test/cgi/assets/images/image.png +1 -0
- data/test/cgi/assets/index.html +1 -0
- data/test/cgi/assets/javascripts/app.js +1 -0
- data/test/cgi/assets/stylesheets/app.css +1 -0
- data/test/cgi/lighttpd.conf +26 -0
- data/test/cgi/rackup_stub.rb +6 -0
- data/test/cgi/sample_rackup.ru +5 -0
- data/test/cgi/test +9 -0
- data/test/cgi/test+directory/test+file +1 -0
- data/test/cgi/test.fcgi +8 -0
- data/test/cgi/test.ru +5 -0
- data/test/gemloader.rb +10 -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_null_byte +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/three_files_three_fields +31 -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_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
- data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
- data/test/spec_body_proxy.rb +85 -0
- data/test/spec_builder.rb +223 -0
- data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
- data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
- data/test/spec_chunked.rb +101 -0
- data/test/spec_commonlogger.rb +93 -0
- data/test/spec_conditionalget.rb +102 -0
- data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
- data/test/spec_content_length.rb +85 -0
- data/test/spec_content_type.rb +45 -0
- data/test/spec_deflater.rb +339 -0
- data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
- data/test/spec_etag.rb +107 -0
- data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +72 -0
- data/test/spec_head.rb +45 -0
- data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
- data/test/spec_lobster.rb +58 -0
- data/test/spec_lock.rb +164 -0
- data/test/spec_logger.rb +23 -0
- data/test/spec_methodoverride.rb +95 -0
- data/test/spec_mime.rb +51 -0
- data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
- data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
- data/test/spec_multipart.rb +600 -0
- data/test/spec_nulllogger.rb +20 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1227 -0
- data/test/spec_response.rb +407 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_runtime.rb +49 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_session_abstract_id.rb +53 -0
- data/test/spec_session_cookie.rb +410 -0
- data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
- data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
- data/test/spec_showexceptions.rb +85 -0
- data/test/spec_showstatus.rb +103 -0
- data/test/spec_static.rb +145 -0
- data/test/spec_tempfile_reaper.rb +63 -0
- data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
- data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
- data/test/spec_utils.rb +647 -0
- data/test/spec_version.rb +17 -0
- data/test/spec_webrick.rb +184 -0
- data/test/static/another/index.html +1 -0
- data/test/static/index.html +1 -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 +220 -239
- data/RDOX +0 -0
- data/README +0 -592
- data/lib/rack/adapter/camping.rb +0 -22
- data/test/spec_auth.rb +0 -57
- data/test/spec_rack_builder.rb +0 -84
- data/test/spec_rack_camping.rb +0 -55
- data/test/spec_rack_chunked.rb +0 -62
- data/test/spec_rack_commonlogger.rb +0 -61
- data/test/spec_rack_conditionalget.rb +0 -41
- data/test/spec_rack_content_length.rb +0 -43
- data/test/spec_rack_content_type.rb +0 -30
- data/test/spec_rack_deflater.rb +0 -127
- data/test/spec_rack_etag.rb +0 -17
- data/test/spec_rack_file.rb +0 -75
- data/test/spec_rack_handler.rb +0 -43
- data/test/spec_rack_head.rb +0 -30
- data/test/spec_rack_lobster.rb +0 -45
- data/test/spec_rack_lock.rb +0 -38
- data/test/spec_rack_logger.rb +0 -21
- data/test/spec_rack_methodoverride.rb +0 -60
- data/test/spec_rack_nulllogger.rb +0 -13
- data/test/spec_rack_recursive.rb +0 -77
- data/test/spec_rack_request.rb +0 -594
- data/test/spec_rack_response.rb +0 -221
- data/test/spec_rack_rewindable_input.rb +0 -118
- data/test/spec_rack_runtime.rb +0 -35
- data/test/spec_rack_sendfile.rb +0 -86
- data/test/spec_rack_session_cookie.rb +0 -92
- data/test/spec_rack_showexceptions.rb +0 -21
- data/test/spec_rack_showstatus.rb +0 -72
- data/test/spec_rack_static.rb +0 -37
- data/test/spec_rack_utils.rb +0 -557
- data/test/spec_rack_webrick.rb +0 -130
- data/test/spec_rackup.rb +0 -164
data/lib/rack/mock.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'uri'
|
|
2
2
|
require 'stringio'
|
|
3
|
+
require 'rack'
|
|
3
4
|
require 'rack/lint'
|
|
4
5
|
require 'rack/utils'
|
|
5
6
|
require 'rack/response'
|
|
@@ -8,12 +9,12 @@ module Rack
|
|
|
8
9
|
# Rack::MockRequest helps testing your Rack application without
|
|
9
10
|
# actually using HTTP.
|
|
10
11
|
#
|
|
11
|
-
# After performing a request on a URL with get/post/put/delete, it
|
|
12
|
+
# After performing a request on a URL with get/post/put/patch/delete, it
|
|
12
13
|
# returns a MockResponse with useful helper methods for effective
|
|
13
14
|
# testing.
|
|
14
15
|
#
|
|
15
16
|
# You can pass a hash with additional configuration to the
|
|
16
|
-
# get/post/put/delete.
|
|
17
|
+
# get/post/put/patch/delete.
|
|
17
18
|
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
|
|
18
19
|
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
|
|
19
20
|
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
|
|
@@ -40,7 +41,7 @@ module Rack
|
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
DEFAULT_ENV = {
|
|
43
|
-
"rack.version" =>
|
|
44
|
+
"rack.version" => Rack::VERSION,
|
|
44
45
|
"rack.input" => StringIO.new,
|
|
45
46
|
"rack.errors" => StringIO.new,
|
|
46
47
|
"rack.multithread" => true,
|
|
@@ -52,10 +53,13 @@ module Rack
|
|
|
52
53
|
@app = app
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
def get(uri, opts={})
|
|
56
|
-
def post(uri, opts={})
|
|
57
|
-
def put(uri, opts={})
|
|
58
|
-
def
|
|
56
|
+
def get(uri, opts={}) request("GET", uri, opts) end
|
|
57
|
+
def post(uri, opts={}) request("POST", uri, opts) end
|
|
58
|
+
def put(uri, opts={}) request("PUT", uri, opts) end
|
|
59
|
+
def patch(uri, opts={}) request("PATCH", uri, opts) end
|
|
60
|
+
def delete(uri, opts={}) request("DELETE", uri, opts) end
|
|
61
|
+
def head(uri, opts={}) request("HEAD", uri, opts) end
|
|
62
|
+
def options(uri, opts={}) request("OPTIONS", uri, opts) end
|
|
59
63
|
|
|
60
64
|
def request(method="GET", uri="", opts={})
|
|
61
65
|
env = self.class.env_for(uri, opts.merge(:method => method))
|
|
@@ -67,25 +71,29 @@ module Rack
|
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
errors = env["rack.errors"]
|
|
70
|
-
|
|
74
|
+
status, headers, body = app.call(env)
|
|
75
|
+
MockResponse.new(status, headers, body, errors)
|
|
76
|
+
ensure
|
|
77
|
+
body.close if body.respond_to?(:close)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# For historical reasons, we're pinning to RFC 2396. It's easier for users
|
|
81
|
+
# and we get support from ruby 1.8 to 2.2 using this method.
|
|
82
|
+
def self.parse_uri_rfc2396(uri)
|
|
83
|
+
@parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
|
|
84
|
+
@parser.parse(uri)
|
|
71
85
|
end
|
|
72
86
|
|
|
73
87
|
# Return the Rack environment used for a request to +uri+.
|
|
74
88
|
def self.env_for(uri="", opts={})
|
|
75
|
-
uri =
|
|
89
|
+
uri = parse_uri_rfc2396(uri)
|
|
76
90
|
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
|
|
77
91
|
|
|
78
92
|
env = DEFAULT_ENV.dup
|
|
79
93
|
|
|
80
|
-
env
|
|
81
|
-
env["SERVER_NAME"] = uri.host || "example.org"
|
|
82
|
-
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
|
83
|
-
env["QUERY_STRING"] = uri.query.to_s
|
|
84
|
-
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
|
85
|
-
env["rack.url_scheme"] = uri.scheme || "http"
|
|
86
|
-
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
|
|
94
|
+
env_with_encoding(env, opts, uri)
|
|
87
95
|
|
|
88
|
-
env[
|
|
96
|
+
env[SCRIPT_NAME] = opts[:script_name] || ""
|
|
89
97
|
|
|
90
98
|
if opts[:fatal]
|
|
91
99
|
env["rack.errors"] = FatalWarner.new
|
|
@@ -94,10 +102,10 @@ module Rack
|
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
if params = opts[:params]
|
|
97
|
-
if env[
|
|
105
|
+
if env[REQUEST_METHOD] == "GET"
|
|
98
106
|
params = Utils.parse_nested_query(params) if params.is_a?(String)
|
|
99
|
-
params.update(Utils.parse_nested_query(env[
|
|
100
|
-
env[
|
|
107
|
+
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
|
|
108
|
+
env[QUERY_STRING] = Utils.build_nested_query(params)
|
|
101
109
|
elsif !opts.has_key?(:input)
|
|
102
110
|
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
|
103
111
|
if params.is_a?(Hash)
|
|
@@ -134,56 +142,73 @@ module Rack
|
|
|
134
142
|
|
|
135
143
|
env
|
|
136
144
|
end
|
|
145
|
+
|
|
146
|
+
if "<3".respond_to? :b
|
|
147
|
+
def self.env_with_encoding(env, opts, uri)
|
|
148
|
+
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : "GET").b
|
|
149
|
+
env["SERVER_NAME"] = (uri.host || "example.org").b
|
|
150
|
+
env["SERVER_PORT"] = (uri.port ? uri.port.to_s : "80").b
|
|
151
|
+
env[QUERY_STRING] = (uri.query.to_s).b
|
|
152
|
+
env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
|
|
153
|
+
env["rack.url_scheme"] = (uri.scheme || "http").b
|
|
154
|
+
env["HTTPS"] = (env["rack.url_scheme"] == "https" ? "on" : "off").b
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
def self.env_with_encoding(env, opts, uri)
|
|
158
|
+
env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : "GET"
|
|
159
|
+
env["SERVER_NAME"] = uri.host || "example.org"
|
|
160
|
+
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
|
|
161
|
+
env[QUERY_STRING] = uri.query.to_s
|
|
162
|
+
env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
|
|
163
|
+
env["rack.url_scheme"] = uri.scheme || "http"
|
|
164
|
+
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
137
167
|
end
|
|
138
168
|
|
|
139
169
|
# Rack::MockResponse provides useful helpers for testing your apps.
|
|
140
170
|
# Usually, you don't create the MockResponse on your own, but use
|
|
141
171
|
# MockRequest.
|
|
142
172
|
|
|
143
|
-
class MockResponse
|
|
144
|
-
def initialize(status, headers, body, errors=StringIO.new(""))
|
|
145
|
-
@status = status.to_i
|
|
146
|
-
|
|
147
|
-
@original_headers = headers
|
|
148
|
-
@headers = Rack::Utils::HeaderHash.new
|
|
149
|
-
headers.each { |field, values|
|
|
150
|
-
@headers[field] = values
|
|
151
|
-
@headers[field] = "" if values.empty?
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@body = ""
|
|
155
|
-
body.each { |part| @body << part }
|
|
156
|
-
|
|
157
|
-
@errors = errors.string if errors.respond_to?(:string)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Status
|
|
161
|
-
attr_reader :status
|
|
162
|
-
|
|
173
|
+
class MockResponse < Rack::Response
|
|
163
174
|
# Headers
|
|
164
|
-
attr_reader :
|
|
175
|
+
attr_reader :original_headers
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
177
|
+
# Errors
|
|
178
|
+
attr_accessor :errors
|
|
169
179
|
|
|
180
|
+
def initialize(status, headers, body, errors=StringIO.new(""))
|
|
181
|
+
@original_headers = headers
|
|
182
|
+
@errors = errors.string if errors.respond_to?(:string)
|
|
183
|
+
@body_string = nil
|
|
170
184
|
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
super(body, status, headers)
|
|
186
|
+
end
|
|
173
187
|
|
|
174
188
|
def =~(other)
|
|
175
|
-
|
|
189
|
+
body =~ other
|
|
176
190
|
end
|
|
177
191
|
|
|
178
192
|
def match(other)
|
|
179
|
-
|
|
193
|
+
body.match other
|
|
180
194
|
end
|
|
181
195
|
|
|
196
|
+
def body
|
|
197
|
+
# FIXME: apparently users of MockResponse expect the return value of
|
|
198
|
+
# MockResponse#body to be a string. However, the real response object
|
|
199
|
+
# returns the body as a list.
|
|
200
|
+
#
|
|
201
|
+
# See spec_showstatus.rb:
|
|
202
|
+
#
|
|
203
|
+
# should "not replace existing messages" do
|
|
204
|
+
# ...
|
|
205
|
+
# res.body.should == "foo!"
|
|
206
|
+
# end
|
|
207
|
+
super.join
|
|
208
|
+
end
|
|
182
209
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
include Response::Helpers
|
|
210
|
+
def empty?
|
|
211
|
+
[201, 204, 205, 304].include? status
|
|
212
|
+
end
|
|
188
213
|
end
|
|
189
214
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
module Multipart
|
|
3
|
+
class Generator
|
|
4
|
+
def initialize(params, first = true)
|
|
5
|
+
@params, @first = params, first
|
|
6
|
+
|
|
7
|
+
if @first && !@params.is_a?(Hash)
|
|
8
|
+
raise ArgumentError, "value must be a Hash"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dump
|
|
13
|
+
return nil if @first && !multipart?
|
|
14
|
+
return flattened_params if !@first
|
|
15
|
+
|
|
16
|
+
flattened_params.map do |name, file|
|
|
17
|
+
if file.respond_to?(:original_filename)
|
|
18
|
+
::File.open(file.path, "rb") do |f|
|
|
19
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
|
20
|
+
content_for_tempfile(f, file, name)
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
content_for_other(file, name)
|
|
24
|
+
end
|
|
25
|
+
end.join + "--#{MULTIPART_BOUNDARY}--\r"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def multipart?
|
|
30
|
+
multipart = false
|
|
31
|
+
|
|
32
|
+
query = lambda { |value|
|
|
33
|
+
case value
|
|
34
|
+
when Array
|
|
35
|
+
value.each(&query)
|
|
36
|
+
when Hash
|
|
37
|
+
value.values.each(&query)
|
|
38
|
+
when Rack::Multipart::UploadedFile
|
|
39
|
+
multipart = true
|
|
40
|
+
end
|
|
41
|
+
}
|
|
42
|
+
@params.values.each(&query)
|
|
43
|
+
|
|
44
|
+
multipart
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flattened_params
|
|
48
|
+
@flattened_params ||= begin
|
|
49
|
+
h = Hash.new
|
|
50
|
+
@params.each do |key, value|
|
|
51
|
+
k = @first ? key.to_s : "[#{key}]"
|
|
52
|
+
|
|
53
|
+
case value
|
|
54
|
+
when Array
|
|
55
|
+
value.map { |v|
|
|
56
|
+
Multipart.build_multipart(v, false).each { |subkey, subvalue|
|
|
57
|
+
h["#{k}[]#{subkey}"] = subvalue
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
when Hash
|
|
61
|
+
Multipart.build_multipart(value, false).each { |subkey, subvalue|
|
|
62
|
+
h[k + subkey] = subvalue
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
h[k] = value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
h
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def content_for_tempfile(io, file, name)
|
|
73
|
+
<<-EOF
|
|
74
|
+
--#{MULTIPART_BOUNDARY}\r
|
|
75
|
+
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
|
|
76
|
+
Content-Type: #{file.content_type}\r
|
|
77
|
+
Content-Length: #{::File.stat(file.path).size}\r
|
|
78
|
+
\r
|
|
79
|
+
#{io.read}\r
|
|
80
|
+
EOF
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def content_for_other(file, name)
|
|
84
|
+
<<-EOF
|
|
85
|
+
--#{MULTIPART_BOUNDARY}\r
|
|
86
|
+
Content-Disposition: form-data; name="#{name}"\r
|
|
87
|
+
\r
|
|
88
|
+
#{file}\r
|
|
89
|
+
EOF
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
require 'rack/utils'
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
module Multipart
|
|
5
|
+
class MultipartPartLimitError < Errno::EMFILE; end
|
|
6
|
+
|
|
7
|
+
class Parser
|
|
8
|
+
BUFSIZE = 16384
|
|
9
|
+
DUMMY = Struct.new(:parse).new
|
|
10
|
+
|
|
11
|
+
def self.create(env)
|
|
12
|
+
return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART
|
|
13
|
+
|
|
14
|
+
io = env['rack.input']
|
|
15
|
+
io.rewind
|
|
16
|
+
|
|
17
|
+
content_length = env['CONTENT_LENGTH']
|
|
18
|
+
content_length = content_length.to_i if content_length
|
|
19
|
+
|
|
20
|
+
tempfile = env['rack.multipart.tempfile_factory'] ||
|
|
21
|
+
lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))]) }
|
|
22
|
+
bufsize = env['rack.multipart.buffer_size'] || BUFSIZE
|
|
23
|
+
|
|
24
|
+
new($1, io, content_length, env, tempfile, bufsize)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(boundary, io, content_length, env, tempfile, bufsize)
|
|
28
|
+
@buf = ""
|
|
29
|
+
|
|
30
|
+
if @buf.respond_to? :force_encoding
|
|
31
|
+
@buf.force_encoding Encoding::ASCII_8BIT
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@params = Utils::KeySpaceConstrainedParams.new
|
|
35
|
+
@boundary = "--#{boundary}"
|
|
36
|
+
@io = io
|
|
37
|
+
@content_length = content_length
|
|
38
|
+
@boundary_size = Utils.bytesize(@boundary) + EOL.size
|
|
39
|
+
@env = env
|
|
40
|
+
@tempfile = tempfile
|
|
41
|
+
@bufsize = bufsize
|
|
42
|
+
|
|
43
|
+
if @content_length
|
|
44
|
+
@content_length -= @boundary_size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
|
|
48
|
+
@full_boundary = @boundary + EOL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse
|
|
52
|
+
fast_forward_to_first_boundary
|
|
53
|
+
|
|
54
|
+
opened_files = 0
|
|
55
|
+
loop do
|
|
56
|
+
|
|
57
|
+
head, filename, content_type, name, body =
|
|
58
|
+
get_current_head_and_filename_and_content_type_and_name_and_body
|
|
59
|
+
|
|
60
|
+
if Utils.multipart_part_limit > 0
|
|
61
|
+
opened_files += 1 if filename
|
|
62
|
+
raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Save the rest.
|
|
66
|
+
if i = @buf.index(rx)
|
|
67
|
+
body << @buf.slice!(0, i)
|
|
68
|
+
@buf.slice!(0, @boundary_size+2)
|
|
69
|
+
|
|
70
|
+
@content_length = -1 if $1 == "--"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
get_data(filename, body, content_type, name, head) do |data|
|
|
74
|
+
tag_multipart_encoding(filename, content_type, name, data)
|
|
75
|
+
|
|
76
|
+
Utils.normalize_params(@params, name, data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# break if we're at the end of a buffer, but not if it is the end of a field
|
|
80
|
+
break if (@buf.empty? && $1 != EOL) || @content_length == -1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@io.rewind
|
|
84
|
+
|
|
85
|
+
@params.to_params_hash
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
def full_boundary; @full_boundary; end
|
|
90
|
+
|
|
91
|
+
def rx; @rx; end
|
|
92
|
+
|
|
93
|
+
def fast_forward_to_first_boundary
|
|
94
|
+
loop do
|
|
95
|
+
content = @io.read(@bufsize)
|
|
96
|
+
raise EOFError, "bad content body" unless content
|
|
97
|
+
@buf << content
|
|
98
|
+
|
|
99
|
+
while @buf.gsub!(/\A([^\n]*\n)/, '')
|
|
100
|
+
read_buffer = $1
|
|
101
|
+
return if read_buffer == full_boundary
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def get_current_head_and_filename_and_content_type_and_name_and_body
|
|
109
|
+
head = nil
|
|
110
|
+
body = ''
|
|
111
|
+
|
|
112
|
+
if body.respond_to? :force_encoding
|
|
113
|
+
body.force_encoding Encoding::ASCII_8BIT
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
filename = content_type = name = nil
|
|
117
|
+
|
|
118
|
+
until head && @buf =~ rx
|
|
119
|
+
if !head && i = @buf.index(EOL+EOL)
|
|
120
|
+
head = @buf.slice!(0, i+2) # First \r\n
|
|
121
|
+
|
|
122
|
+
@buf.slice!(0, 2) # Second \r\n
|
|
123
|
+
|
|
124
|
+
content_type = head[MULTIPART_CONTENT_TYPE, 1]
|
|
125
|
+
name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
|
|
126
|
+
|
|
127
|
+
filename = get_filename(head)
|
|
128
|
+
|
|
129
|
+
if name.nil? || name.empty? && filename
|
|
130
|
+
name = filename
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if filename
|
|
134
|
+
(@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
|
|
135
|
+
body.binmode if body.respond_to?(:binmode)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
next
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Save the read body part.
|
|
142
|
+
if head && (@boundary_size+4 < @buf.size)
|
|
143
|
+
body << @buf.slice!(0, @buf.size - (@boundary_size+4))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
|
|
147
|
+
raise EOFError, "bad content body" if content.nil? || content.empty?
|
|
148
|
+
|
|
149
|
+
@buf << content
|
|
150
|
+
@content_length -= content.size if @content_length
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
[head, filename, content_type, name, body]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get_filename(head)
|
|
157
|
+
filename = nil
|
|
158
|
+
case head
|
|
159
|
+
when RFC2183
|
|
160
|
+
filename = Hash[head.scan(DISPPARM)]['filename']
|
|
161
|
+
filename = $1 if filename and filename =~ /^"(.*)"$/
|
|
162
|
+
when BROKEN_QUOTED, BROKEN_UNQUOTED
|
|
163
|
+
filename = $1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
return unless filename
|
|
167
|
+
|
|
168
|
+
if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
|
|
169
|
+
filename = Utils.unescape(filename)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
scrub_filename filename
|
|
173
|
+
|
|
174
|
+
if filename !~ /\\[^\\"]/
|
|
175
|
+
filename = filename.gsub(/\\(.)/, '\1')
|
|
176
|
+
end
|
|
177
|
+
filename
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if "<3".respond_to? :valid_encoding?
|
|
181
|
+
def scrub_filename(filename)
|
|
182
|
+
unless filename.valid_encoding?
|
|
183
|
+
# FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
|
|
184
|
+
# We can remove it after they are dropped
|
|
185
|
+
filename.force_encoding(Encoding::ASCII_8BIT)
|
|
186
|
+
filename.encode!(:invalid => :replace, :undef => :replace)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
CHARSET = "charset"
|
|
191
|
+
TEXT_PLAIN = "text/plain"
|
|
192
|
+
|
|
193
|
+
def tag_multipart_encoding(filename, content_type, name, body)
|
|
194
|
+
name.force_encoding Encoding::UTF_8
|
|
195
|
+
|
|
196
|
+
return if filename
|
|
197
|
+
|
|
198
|
+
encoding = Encoding::UTF_8
|
|
199
|
+
|
|
200
|
+
if content_type
|
|
201
|
+
list = content_type.split(';')
|
|
202
|
+
type_subtype = list.first
|
|
203
|
+
type_subtype.strip!
|
|
204
|
+
if TEXT_PLAIN == type_subtype
|
|
205
|
+
rest = list.drop 1
|
|
206
|
+
rest.each do |param|
|
|
207
|
+
k,v = param.split('=', 2)
|
|
208
|
+
k.strip!
|
|
209
|
+
v.strip!
|
|
210
|
+
encoding = Encoding.find v if k == CHARSET
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
name.force_encoding encoding
|
|
216
|
+
body.force_encoding encoding
|
|
217
|
+
end
|
|
218
|
+
else
|
|
219
|
+
def scrub_filename(filename)
|
|
220
|
+
end
|
|
221
|
+
def tag_multipart_encoding(filename, content_type, name, body)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def get_data(filename, body, content_type, name, head)
|
|
226
|
+
data = body
|
|
227
|
+
if filename == ""
|
|
228
|
+
# filename is blank which means no file has been selected
|
|
229
|
+
return
|
|
230
|
+
elsif filename
|
|
231
|
+
body.rewind if body.respond_to?(:rewind)
|
|
232
|
+
|
|
233
|
+
# Take the basename of the upload's original filename.
|
|
234
|
+
# This handles the full Windows paths given by Internet Explorer
|
|
235
|
+
# (and perhaps other broken user agents) without affecting
|
|
236
|
+
# those which give the lone filename.
|
|
237
|
+
filename = filename.split(/[\/\\]/).last
|
|
238
|
+
|
|
239
|
+
data = {:filename => filename, :type => content_type,
|
|
240
|
+
:name => name, :tempfile => body, :head => head}
|
|
241
|
+
elsif !filename && content_type && body.is_a?(IO)
|
|
242
|
+
body.rewind
|
|
243
|
+
|
|
244
|
+
# Generic multipart cases, not coming from a form
|
|
245
|
+
data = {:type => content_type,
|
|
246
|
+
:name => name, :tempfile => body, :head => head}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
yield data
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -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,34 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
# A multipart form data parser, adapted from IOWA.
|
|
3
|
+
#
|
|
4
|
+
# Usually, Rack::Request#POST takes care of calling this.
|
|
5
|
+
module Multipart
|
|
6
|
+
autoload :UploadedFile, 'rack/multipart/uploaded_file'
|
|
7
|
+
autoload :Parser, 'rack/multipart/parser'
|
|
8
|
+
autoload :Generator, 'rack/multipart/generator'
|
|
9
|
+
|
|
10
|
+
EOL = "\r\n"
|
|
11
|
+
MULTIPART_BOUNDARY = "AaB03x"
|
|
12
|
+
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
|
|
13
|
+
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
|
|
14
|
+
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
|
|
15
|
+
DISPPARM = /;\s*(#{TOKEN})=("(?:\\"|[^"])*"|#{TOKEN})/
|
|
16
|
+
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
|
|
17
|
+
BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
|
|
18
|
+
BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
|
|
19
|
+
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
|
|
20
|
+
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name="?([^\";]*)"?/ni
|
|
21
|
+
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def parse_multipart(env)
|
|
25
|
+
Parser.create(env).parse
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_multipart(params, first = true)
|
|
29
|
+
Generator.new(params, first).dump
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/rack/nulllogger.rb
CHANGED
|
@@ -9,10 +9,29 @@ module Rack
|
|
|
9
9
|
@app.call(env)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def info(progname = nil, &block);
|
|
12
|
+
def info(progname = nil, &block); end
|
|
13
13
|
def debug(progname = nil, &block); end
|
|
14
|
-
def warn(progname = nil, &block);
|
|
14
|
+
def warn(progname = nil, &block); end
|
|
15
15
|
def error(progname = nil, &block); end
|
|
16
16
|
def fatal(progname = nil, &block); end
|
|
17
|
+
def unknown(progname = nil, &block); end
|
|
18
|
+
def info? ; end
|
|
19
|
+
def debug? ; end
|
|
20
|
+
def warn? ; end
|
|
21
|
+
def error? ; end
|
|
22
|
+
def fatal? ; end
|
|
23
|
+
def level ; end
|
|
24
|
+
def progname ; end
|
|
25
|
+
def datetime_format ; end
|
|
26
|
+
def formatter ; end
|
|
27
|
+
def sev_threshold ; end
|
|
28
|
+
def level=(level); end
|
|
29
|
+
def progname=(progname); end
|
|
30
|
+
def datetime_format=(datetime_format); end
|
|
31
|
+
def formatter=(formatter); end
|
|
32
|
+
def sev_threshold=(sev_threshold); end
|
|
33
|
+
def close ; end
|
|
34
|
+
def add(severity, message = nil, progname = nil, &block); end
|
|
35
|
+
def <<(msg); end
|
|
17
36
|
end
|
|
18
37
|
end
|
data/lib/rack/recursive.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Rack
|
|
|
14
14
|
@url = URI(url)
|
|
15
15
|
@env = env
|
|
16
16
|
|
|
17
|
-
@env[
|
|
18
|
-
@env[
|
|
17
|
+
@env[PATH_INFO] = @url.path
|
|
18
|
+
@env[QUERY_STRING] = @url.query if @url.query
|
|
19
19
|
@env["HTTP_HOST"] = @url.host if @url.host
|
|
20
20
|
@env["HTTP_PORT"] = @url.port if @url.port
|
|
21
21
|
@env["rack.url_scheme"] = @url.scheme if @url.scheme
|
|
@@ -35,7 +35,11 @@ module Rack
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def call(env)
|
|
38
|
-
|
|
38
|
+
dup._call(env)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def _call(env)
|
|
42
|
+
@script_name = env[SCRIPT_NAME]
|
|
39
43
|
@app.call(env.merge('rack.recursive.include' => method(:include)))
|
|
40
44
|
rescue ForwardRequest => req
|
|
41
45
|
call(env.merge(req.env))
|
|
@@ -47,8 +51,9 @@ module Rack
|
|
|
47
51
|
raise ArgumentError, "can only include below #{@script_name}, not #{path}"
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
env = env.merge(
|
|
51
|
-
|
|
54
|
+
env = env.merge(PATH_INFO => path,
|
|
55
|
+
SCRIPT_NAME => @script_name,
|
|
56
|
+
REQUEST_METHOD => "GET",
|
|
52
57
|
"CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "",
|
|
53
58
|
"rack.input" => StringIO.new(""))
|
|
54
59
|
@app.call(env)
|