lack 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rackup +5 -0
  3. data/lib/rack.rb +26 -0
  4. data/lib/rack/body_proxy.rb +39 -0
  5. data/lib/rack/builder.rb +166 -0
  6. data/lib/rack/handler.rb +63 -0
  7. data/lib/rack/handler/webrick.rb +120 -0
  8. data/lib/rack/mime.rb +661 -0
  9. data/lib/rack/mock.rb +198 -0
  10. data/lib/rack/multipart.rb +31 -0
  11. data/lib/rack/multipart/generator.rb +93 -0
  12. data/lib/rack/multipart/parser.rb +239 -0
  13. data/lib/rack/multipart/uploaded_file.rb +34 -0
  14. data/lib/rack/request.rb +394 -0
  15. data/lib/rack/response.rb +160 -0
  16. data/lib/rack/server.rb +258 -0
  17. data/lib/rack/server/options.rb +121 -0
  18. data/lib/rack/utils.rb +653 -0
  19. data/lib/rack/version.rb +3 -0
  20. data/spec/spec_helper.rb +1 -0
  21. data/test/builder/anything.rb +5 -0
  22. data/test/builder/comment.ru +4 -0
  23. data/test/builder/end.ru +5 -0
  24. data/test/builder/line.ru +1 -0
  25. data/test/builder/options.ru +2 -0
  26. data/test/multipart/bad_robots +259 -0
  27. data/test/multipart/binary +0 -0
  28. data/test/multipart/content_type_and_no_filename +6 -0
  29. data/test/multipart/empty +10 -0
  30. data/test/multipart/fail_16384_nofile +814 -0
  31. data/test/multipart/file1.txt +1 -0
  32. data/test/multipart/filename_and_modification_param +7 -0
  33. data/test/multipart/filename_and_no_name +6 -0
  34. data/test/multipart/filename_with_escaped_quotes +6 -0
  35. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  36. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  37. data/test/multipart/filename_with_unescaped_percentages +6 -0
  38. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  39. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  40. data/test/multipart/filename_with_unescaped_quotes +6 -0
  41. data/test/multipart/ie +6 -0
  42. data/test/multipart/invalid_character +6 -0
  43. data/test/multipart/mixed_files +21 -0
  44. data/test/multipart/nested +10 -0
  45. data/test/multipart/none +9 -0
  46. data/test/multipart/semicolon +6 -0
  47. data/test/multipart/text +15 -0
  48. data/test/multipart/webkit +32 -0
  49. data/test/rackup/config.ru +31 -0
  50. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  51. data/test/spec_body_proxy.rb +69 -0
  52. data/test/spec_builder.rb +223 -0
  53. data/test/spec_chunked.rb +101 -0
  54. data/test/spec_file.rb +221 -0
  55. data/test/spec_handler.rb +59 -0
  56. data/test/spec_head.rb +45 -0
  57. data/test/spec_lint.rb +522 -0
  58. data/test/spec_mime.rb +51 -0
  59. data/test/spec_mock.rb +277 -0
  60. data/test/spec_multipart.rb +547 -0
  61. data/test/spec_recursive.rb +72 -0
  62. data/test/spec_request.rb +1199 -0
  63. data/test/spec_response.rb +343 -0
  64. data/test/spec_rewindable_input.rb +118 -0
  65. data/test/spec_sendfile.rb +130 -0
  66. data/test/spec_server.rb +167 -0
  67. data/test/spec_utils.rb +635 -0
  68. data/test/spec_webrick.rb +184 -0
  69. data/test/testrequest.rb +78 -0
  70. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  71. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  72. metadata +240 -0
@@ -0,0 +1,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