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,198 @@
1
+ require 'uri'
2
+ require 'stringio'
3
+ require 'rack'
4
+ require 'rack/lint'
5
+ require 'rack/utils'
6
+ require 'rack/response'
7
+
8
+ module Rack
9
+ # Rack::MockRequest helps testing your Rack application without
10
+ # actually using HTTP.
11
+ #
12
+ # After performing a request on a URL with get/post/put/patch/delete, it
13
+ # returns a MockResponse with useful helper methods for effective
14
+ # testing.
15
+ #
16
+ # You can pass a hash with additional configuration to the
17
+ # get/post/put/patch/delete.
18
+ # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
19
+ # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
20
+ # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
21
+
22
+ class MockRequest
23
+ class FatalWarning < RuntimeError
24
+ end
25
+
26
+ class FatalWarner
27
+ def puts(warning)
28
+ raise FatalWarning, warning
29
+ end
30
+
31
+ def write(warning)
32
+ raise FatalWarning, warning
33
+ end
34
+
35
+ def flush
36
+ end
37
+
38
+ def string
39
+ ""
40
+ end
41
+ end
42
+
43
+ DEFAULT_ENV = {
44
+ "rack.version" => Rack::VERSION,
45
+ "rack.input" => StringIO.new,
46
+ "rack.errors" => StringIO.new,
47
+ "rack.multithread" => true,
48
+ "rack.multiprocess" => true,
49
+ "rack.run_once" => false,
50
+ }
51
+
52
+ def initialize(app)
53
+ @app = app
54
+ end
55
+
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
63
+
64
+ def request(method="GET", uri="", opts={})
65
+ env = self.class.env_for(uri, opts.merge(:method => method))
66
+
67
+ if opts[:lint]
68
+ app = Rack::Lint.new(@app)
69
+ else
70
+ app = @app
71
+ end
72
+
73
+ errors = env["rack.errors"]
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)
85
+ end
86
+
87
+ # Return the Rack environment used for a request to +uri+.
88
+ def self.env_for(uri="", opts={})
89
+ uri = parse_uri_rfc2396(uri)
90
+ uri.path = "/#{uri.path}" unless uri.path[0] == ?/
91
+
92
+ env = DEFAULT_ENV.dup
93
+
94
+ env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
95
+ env["SERVER_NAME"] = uri.host || "example.org"
96
+ env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
97
+ env["QUERY_STRING"] = uri.query.to_s
98
+ env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
99
+ env["rack.url_scheme"] = uri.scheme || "http"
100
+ env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
101
+
102
+ env["SCRIPT_NAME"] = opts[:script_name] || ""
103
+
104
+ if opts[:fatal]
105
+ env["rack.errors"] = FatalWarner.new
106
+ else
107
+ env["rack.errors"] = StringIO.new
108
+ end
109
+
110
+ if params = opts[:params]
111
+ if env["REQUEST_METHOD"] == "GET"
112
+ params = Utils.parse_nested_query(params) if params.is_a?(String)
113
+ params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
114
+ env["QUERY_STRING"] = Utils.build_nested_query(params)
115
+ elsif !opts.has_key?(:input)
116
+ opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
117
+ if params.is_a?(Hash)
118
+ if data = Utils::Multipart.build_multipart(params)
119
+ opts[:input] = data
120
+ opts["CONTENT_LENGTH"] ||= data.length.to_s
121
+ opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
122
+ else
123
+ opts[:input] = Utils.build_nested_query(params)
124
+ end
125
+ else
126
+ opts[:input] = params
127
+ end
128
+ end
129
+ end
130
+
131
+ empty_str = ""
132
+ empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
133
+ opts[:input] ||= empty_str
134
+ if String === opts[:input]
135
+ rack_input = StringIO.new(opts[:input])
136
+ else
137
+ rack_input = opts[:input]
138
+ end
139
+
140
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
141
+ env['rack.input'] = rack_input
142
+
143
+ env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
144
+
145
+ opts.each { |field, value|
146
+ env[field] = value if String === field
147
+ }
148
+
149
+ env
150
+ end
151
+ end
152
+
153
+ # Rack::MockResponse provides useful helpers for testing your apps.
154
+ # Usually, you don't create the MockResponse on your own, but use
155
+ # MockRequest.
156
+
157
+ class MockResponse < Rack::Response
158
+ # Headers
159
+ attr_reader :original_headers
160
+
161
+ # Errors
162
+ attr_accessor :errors
163
+
164
+ def initialize(status, headers, body, errors=StringIO.new(""))
165
+ @original_headers = headers
166
+ @errors = errors.string if errors.respond_to?(:string)
167
+ @body_string = nil
168
+
169
+ super(body, status, headers)
170
+ end
171
+
172
+ def =~(other)
173
+ body =~ other
174
+ end
175
+
176
+ def match(other)
177
+ body.match other
178
+ end
179
+
180
+ def body
181
+ # FIXME: apparently users of MockResponse expect the return value of
182
+ # MockResponse#body to be a string. However, the real response object
183
+ # returns the body as a list.
184
+ #
185
+ # See spec_showstatus.rb:
186
+ #
187
+ # should "not replace existing messages" do
188
+ # ...
189
+ # res.body.should == "foo!"
190
+ # end
191
+ super.join
192
+ end
193
+
194
+ def empty?
195
+ [201, 204, 205, 304].include? status
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,31 @@
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
+ require_relative "multipart/uploaded_file"
7
+ require_relative "multipart/parser"
8
+ require_relative "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
+ def self.parse_multipart(env)
24
+ Parser.create(env).parse
25
+ end
26
+
27
+ def self.build_multipart(params, first = true)
28
+ Generator.new(params, first).dump
29
+ end
30
+ end
31
+ 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,239 @@
1
+
2
+ module Rack
3
+ module Multipart
4
+ class Parser
5
+ BUFSIZE = 16384
6
+
7
+ DUMMY = Struct.new(:parse).new
8
+
9
+ def self.create(env)
10
+ return DUMMY unless env["CONTENT_TYPE"] =~ MULTIPART
11
+
12
+ io = env["rack.input"]
13
+ io.rewind
14
+
15
+ content_length = env["CONTENT_LENGTH"]
16
+ content_length = content_length.to_i if content_length
17
+
18
+ new($1, io, content_length, env)
19
+ end
20
+
21
+ def initialize(boundary, io, content_length, env)
22
+ @buf = ""
23
+
24
+ if @buf.respond_to? :force_encoding
25
+ @buf.force_encoding Encoding::ASCII_8BIT
26
+ end
27
+
28
+ @params = Utils::KeySpaceConstrainedParams.new
29
+ @boundary = "--#{boundary}"
30
+ @io = io
31
+ @content_length = content_length
32
+ @boundary_size = Utils.bytesize(@boundary) + EOL.size
33
+ @env = env
34
+
35
+ if @content_length
36
+ @content_length -= @boundary_size
37
+ end
38
+
39
+ @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
40
+ @full_boundary = @boundary + EOL
41
+ end
42
+
43
+ def parse
44
+ fast_forward_to_first_boundary
45
+
46
+ loop do
47
+ head, filename, content_type, name, body =
48
+ get_current_head_and_filename_and_content_type_and_name_and_body
49
+
50
+ # Save the rest.
51
+ if i = @buf.index(rx)
52
+ body << @buf.slice!(0, i)
53
+ @buf.slice!(0, @boundary_size+2)
54
+
55
+ @content_length = -1 if $1 == "--"
56
+ end
57
+
58
+ get_data(filename, body, content_type, name, head) do |data|
59
+ tag_multipart_encoding(filename, content_type, name, data)
60
+
61
+ Utils.normalize_params(@params, name, data)
62
+ end
63
+
64
+ # break if we're at the end of a buffer, but not if it is the end of a field
65
+ break if (@buf.empty? && $1 != EOL) || @content_length == -1
66
+ end
67
+
68
+ @io.rewind
69
+
70
+ @params.to_params_hash
71
+ end
72
+
73
+ private
74
+ def full_boundary; @full_boundary; end
75
+
76
+ def rx; @rx; end
77
+
78
+ def fast_forward_to_first_boundary
79
+ loop do
80
+ content = @io.read(BUFSIZE)
81
+ raise EOFError, "bad content body" unless content
82
+ @buf << content
83
+
84
+ while @buf.gsub!(/\A([^\n]*\n)/, "")
85
+ read_buffer = $1
86
+ return if read_buffer == full_boundary
87
+ end
88
+
89
+ raise EOFError, "bad content body" if Utils.bytesize(@buf) >= BUFSIZE
90
+ end
91
+ end
92
+
93
+ def get_current_head_and_filename_and_content_type_and_name_and_body
94
+ head = nil
95
+ body = ""
96
+
97
+ if body.respond_to? :force_encoding
98
+ body.force_encoding Encoding::ASCII_8BIT
99
+ end
100
+
101
+ filename = content_type = name = nil
102
+
103
+ until head && @buf =~ rx
104
+ if !head && i = @buf.index(EOL+EOL)
105
+ head = @buf.slice!(0, i+2) # First \r\n
106
+
107
+ @buf.slice!(0, 2) # Second \r\n
108
+
109
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
110
+ name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
111
+
112
+ filename = get_filename(head)
113
+
114
+ if name.nil? || name.empty? && filename
115
+ name = filename
116
+ end
117
+
118
+ if filename
119
+ extname = ::File.extname(filename)
120
+ (@env["rack.tempfiles"] ||= []) << body = Tempfile.new(["RackMultipart", extname])
121
+ body.binmode if body.respond_to?(:binmode)
122
+ end
123
+
124
+ next
125
+ end
126
+
127
+ # Save the read body part.
128
+ if head && (@boundary_size+4 < @buf.size)
129
+ body << @buf.slice!(0, @buf.size - (@boundary_size+4))
130
+ end
131
+
132
+ content = @io.read(@content_length && BUFSIZE >= @content_length ? @content_length : BUFSIZE)
133
+ raise EOFError, "bad content body" if content.nil? || content.empty?
134
+
135
+ @buf << content
136
+ @content_length -= content.size if @content_length
137
+ end
138
+
139
+ [head, filename, content_type, name, body]
140
+ end
141
+
142
+ def get_filename(head)
143
+ filename = nil
144
+ case head
145
+ when RFC2183
146
+ filename = Hash[head.scan(DISPPARM)]["filename"]
147
+ filename = $1 if filename and filename =~ /^"(.*)"$/
148
+ when BROKEN_QUOTED, BROKEN_UNQUOTED
149
+ filename = $1
150
+ end
151
+
152
+ return unless filename
153
+
154
+ if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
155
+ filename = Utils.unescape(filename)
156
+ end
157
+
158
+ scrub_filename filename
159
+
160
+ if filename !~ /\\[^\\"]/
161
+ filename = filename.gsub(/\\(.)/, '\1')
162
+ end
163
+ filename
164
+ end
165
+
166
+ if "<3".respond_to? :valid_encoding?
167
+ def scrub_filename(filename)
168
+ unless filename.valid_encoding?
169
+ # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
170
+ # We can remove it after they are dropped
171
+ filename.force_encoding(Encoding::ASCII_8BIT)
172
+ filename.encode!(:invalid => :replace, :undef => :replace)
173
+ end
174
+ end
175
+
176
+ CHARSET = "charset"
177
+ TEXT_PLAIN = "text/plain"
178
+
179
+ def tag_multipart_encoding(filename, content_type, name, body)
180
+ name.force_encoding Encoding::UTF_8
181
+
182
+ return if filename
183
+
184
+ encoding = Encoding::UTF_8
185
+
186
+ if content_type
187
+ list = content_type.split(";")
188
+ type_subtype = list.first
189
+ type_subtype.strip!
190
+ if TEXT_PLAIN == type_subtype
191
+ rest = list.drop 1
192
+ rest.each do |param|
193
+ k,v = param.split("=", 2)
194
+ k.strip!
195
+ v.strip!
196
+ encoding = Encoding.find v if k == CHARSET
197
+ end
198
+ end
199
+ end
200
+
201
+ name.force_encoding encoding
202
+ body.force_encoding encoding
203
+ end
204
+ else
205
+ def scrub_filename(filename)
206
+ end
207
+ def tag_multipart_encoding(filename, content_type, name, body)
208
+ end
209
+ end
210
+
211
+ def get_data(filename, body, content_type, name, head)
212
+ data = body
213
+ if filename == ""
214
+ # filename is blank which means no file has been selected
215
+ return
216
+ elsif filename
217
+ body.rewind
218
+
219
+ # Take the basename of the upload's original filename.
220
+ # This handles the full Windows paths given by Internet Explorer
221
+ # (and perhaps other broken user agents) without affecting
222
+ # those which give the lone filename.
223
+ filename = filename.split(/[\/\\]/).last
224
+
225
+ data = {:filename => filename, :type => content_type,
226
+ :name => name, :tempfile => body, :head => head}
227
+ elsif !filename && content_type && body.is_a?(IO)
228
+ body.rewind
229
+
230
+ # Generic multipart cases, not coming from a form
231
+ data = {:type => content_type,
232
+ :name => name, :tempfile => body, :head => head}
233
+ end
234
+
235
+ yield data
236
+ end
237
+ end
238
+ end
239
+ end