rack 1.2.8 → 1.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (89) hide show
  1. data/README +9 -177
  2. data/Rakefile +2 -1
  3. data/SPEC +2 -2
  4. data/lib/rack.rb +2 -13
  5. data/lib/rack/auth/abstract/request.rb +7 -5
  6. data/lib/rack/auth/digest/md5.rb +6 -2
  7. data/lib/rack/auth/digest/params.rb +5 -7
  8. data/lib/rack/auth/digest/request.rb +1 -1
  9. data/lib/rack/backports/uri/common.rb +64 -0
  10. data/lib/rack/builder.rb +60 -3
  11. data/lib/rack/chunked.rb +29 -22
  12. data/lib/rack/conditionalget.rb +35 -16
  13. data/lib/rack/content_length.rb +3 -3
  14. data/lib/rack/deflater.rb +5 -2
  15. data/lib/rack/etag.rb +38 -10
  16. data/lib/rack/file.rb +76 -43
  17. data/lib/rack/handler.rb +13 -7
  18. data/lib/rack/handler/cgi.rb +0 -2
  19. data/lib/rack/handler/fastcgi.rb +13 -4
  20. data/lib/rack/handler/lsws.rb +0 -2
  21. data/lib/rack/handler/mongrel.rb +12 -2
  22. data/lib/rack/handler/scgi.rb +9 -1
  23. data/lib/rack/handler/thin.rb +7 -1
  24. data/lib/rack/handler/webrick.rb +12 -5
  25. data/lib/rack/lint.rb +2 -2
  26. data/lib/rack/lock.rb +29 -3
  27. data/lib/rack/methodoverride.rb +1 -1
  28. data/lib/rack/mime.rb +2 -2
  29. data/lib/rack/mock.rb +28 -33
  30. data/lib/rack/multipart.rb +34 -0
  31. data/lib/rack/multipart/generator.rb +93 -0
  32. data/lib/rack/multipart/parser.rb +164 -0
  33. data/lib/rack/multipart/uploaded_file.rb +30 -0
  34. data/lib/rack/request.rb +55 -19
  35. data/lib/rack/response.rb +10 -8
  36. data/lib/rack/sendfile.rb +14 -18
  37. data/lib/rack/server.rb +55 -8
  38. data/lib/rack/session/abstract/id.rb +233 -22
  39. data/lib/rack/session/cookie.rb +99 -46
  40. data/lib/rack/session/memcache.rb +30 -56
  41. data/lib/rack/session/pool.rb +22 -43
  42. data/lib/rack/showexceptions.rb +40 -11
  43. data/lib/rack/showstatus.rb +9 -2
  44. data/lib/rack/static.rb +29 -9
  45. data/lib/rack/urlmap.rb +6 -1
  46. data/lib/rack/utils.rb +67 -326
  47. data/rack.gemspec +2 -3
  48. data/test/builder/anything.rb +5 -0
  49. data/test/builder/comment.ru +4 -0
  50. data/test/builder/end.ru +3 -0
  51. data/test/builder/options.ru +2 -0
  52. data/test/cgi/lighttpd.conf +1 -1
  53. data/test/cgi/lighttpd.errors +412 -0
  54. data/test/multipart/content_type_and_no_filename +6 -0
  55. data/test/multipart/text +5 -0
  56. data/test/multipart/webkit +32 -0
  57. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  58. data/test/spec_auth_digest.rb +20 -5
  59. data/test/spec_builder.rb +29 -0
  60. data/test/spec_cgi.rb +11 -0
  61. data/test/spec_chunked.rb +1 -1
  62. data/test/spec_commonlogger.rb +1 -1
  63. data/test/spec_conditionalget.rb +47 -0
  64. data/test/spec_content_length.rb +0 -6
  65. data/test/spec_content_type.rb +5 -5
  66. data/test/spec_deflater.rb +46 -2
  67. data/test/spec_etag.rb +68 -1
  68. data/test/spec_fastcgi.rb +11 -0
  69. data/test/spec_file.rb +54 -3
  70. data/test/spec_handler.rb +23 -5
  71. data/test/spec_lint.rb +2 -2
  72. data/test/spec_lock.rb +111 -5
  73. data/test/spec_methodoverride.rb +2 -2
  74. data/test/spec_mock.rb +3 -3
  75. data/test/spec_mongrel.rb +1 -2
  76. data/test/spec_multipart.rb +279 -0
  77. data/test/spec_request.rb +222 -38
  78. data/test/spec_response.rb +9 -3
  79. data/test/spec_server.rb +74 -0
  80. data/test/spec_session_abstract_id.rb +43 -0
  81. data/test/spec_session_cookie.rb +97 -15
  82. data/test/spec_session_memcache.rb +60 -50
  83. data/test/spec_session_pool.rb +63 -40
  84. data/test/spec_showexceptions.rb +64 -0
  85. data/test/spec_static.rb +23 -0
  86. data/test/spec_utils.rb +65 -351
  87. data/test/spec_webrick.rb +23 -4
  88. metadata +35 -15
  89. data/test/spec_auth.rb +0 -57
@@ -0,0 +1,164 @@
1
+ require 'rack/utils'
2
+
3
+ module Rack
4
+ module Multipart
5
+ class Parser
6
+ BUFSIZE = 16384
7
+
8
+ def initialize(env)
9
+ @env = env
10
+ end
11
+
12
+ def parse
13
+ return nil unless setup_parse
14
+
15
+ fast_forward_to_first_boundary
16
+
17
+ loop do
18
+ head, filename, content_type, name, body =
19
+ get_current_head_and_filename_and_content_type_and_name_and_body
20
+
21
+ # Save the rest.
22
+ if i = @buf.index(rx)
23
+ body << @buf.slice!(0, i)
24
+ @buf.slice!(0, @boundary_size+2)
25
+
26
+ @content_length = -1 if $1 == "--"
27
+ end
28
+
29
+ filename, data = get_data(filename, body, content_type, name, head)
30
+
31
+ Utils.normalize_params(@params, name, data) unless data.nil?
32
+
33
+ # break if we're at the end of a buffer, but not if it is the end of a field
34
+ break if (@buf.empty? && $1 != EOL) || @content_length == -1
35
+ end
36
+
37
+ @io.rewind
38
+
39
+ @params
40
+ end
41
+
42
+ private
43
+ def setup_parse
44
+ return false unless @env['CONTENT_TYPE'] =~ MULTIPART
45
+
46
+ @boundary = "--#{$1}"
47
+
48
+ @buf = ""
49
+ @params = {}
50
+
51
+ @content_length = @env['CONTENT_LENGTH'].to_i
52
+ @io = @env['rack.input']
53
+ @io.rewind
54
+
55
+ @boundary_size = Utils.bytesize(@boundary) + EOL.size
56
+
57
+ @content_length -= @boundary_size
58
+ true
59
+ end
60
+
61
+ def full_boundary
62
+ @boundary + EOL
63
+ end
64
+
65
+ def rx
66
+ @rx ||= /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
67
+ end
68
+
69
+ def fast_forward_to_first_boundary
70
+ loop do
71
+ read_buffer = @io.gets
72
+ break if read_buffer == full_boundary
73
+ raise EOFError, "bad content body" if read_buffer.nil?
74
+ end
75
+ end
76
+
77
+ def get_current_head_and_filename_and_content_type_and_name_and_body
78
+ head = nil
79
+ body = ''
80
+ filename = content_type = name = nil
81
+ content = nil
82
+
83
+ until head && @buf =~ rx
84
+ if !head && i = @buf.index(EOL+EOL)
85
+ head = @buf.slice!(0, i+2) # First \r\n
86
+
87
+ @buf.slice!(0, 2) # Second \r\n
88
+
89
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
90
+ name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]
91
+
92
+ filename = get_filename(head)
93
+
94
+ if filename
95
+ body = Tempfile.new("RackMultipart")
96
+ body.binmode if body.respond_to?(:binmode)
97
+ end
98
+
99
+ next
100
+ end
101
+
102
+ # Save the read body part.
103
+ if head && (@boundary_size+4 < @buf.size)
104
+ body << @buf.slice!(0, @buf.size - (@boundary_size+4))
105
+ end
106
+
107
+ content = @io.read(BUFSIZE < @content_length ? BUFSIZE : @content_length)
108
+ raise EOFError, "bad content body" if content.nil? || content.empty?
109
+
110
+ @buf << content
111
+ @content_length -= content.size
112
+ end
113
+
114
+ [head, filename, content_type, name, body]
115
+ end
116
+
117
+ def get_filename(head)
118
+ filename = nil
119
+ if head =~ RFC2183
120
+ filename = Hash[head.scan(DISPPARM)]['filename']
121
+ filename = $1 if filename and filename =~ /^"(.*)"$/
122
+ elsif head =~ BROKEN_QUOTED
123
+ filename = $1
124
+ elsif head =~ BROKEN_UNQUOTED
125
+ filename = $1
126
+ end
127
+
128
+ if filename && filename !~ /\\[^\\"]/
129
+ filename = Utils.unescape(filename).gsub(/\\(.)/, '\1')
130
+ end
131
+ filename
132
+ end
133
+
134
+ def get_data(filename, body, content_type, name, head)
135
+ data = nil
136
+ if filename == ""
137
+ # filename is blank which means no file has been selected
138
+ return data
139
+ elsif filename
140
+ body.rewind
141
+
142
+ # Take the basename of the upload's original filename.
143
+ # This handles the full Windows paths given by Internet Explorer
144
+ # (and perhaps other broken user agents) without affecting
145
+ # those which give the lone filename.
146
+ filename = filename.split(/[\/\\]/).last
147
+
148
+ data = {:filename => filename, :type => content_type,
149
+ :name => name, :tempfile => body, :head => head}
150
+ elsif !filename && content_type && body.is_a?(IO)
151
+ body.rewind
152
+
153
+ # Generic multipart cases, not coming from a form
154
+ data = {:type => content_type,
155
+ :name => name, :tempfile => body, :head => head}
156
+ else
157
+ data = body
158
+ end
159
+
160
+ [filename, data]
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,30 @@
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)
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 method_missing(method_name, *args, &block) #:nodoc:
26
+ @tempfile.__send__(method_name, *args, &block)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -22,14 +22,17 @@ module Rack
22
22
  end
23
23
 
24
24
  def body; @env["rack.input"] end
25
- def scheme; @env["rack.url_scheme"] end
26
25
  def script_name; @env["SCRIPT_NAME"].to_s end
27
26
  def path_info; @env["PATH_INFO"].to_s end
28
- def port; @env["SERVER_PORT"].to_i end
29
27
  def request_method; @env["REQUEST_METHOD"] end
30
28
  def query_string; @env["QUERY_STRING"].to_s end
31
29
  def content_length; @env['CONTENT_LENGTH'] end
32
- def content_type; @env['CONTENT_TYPE'] end
30
+
31
+ def content_type
32
+ content_type = @env['CONTENT_TYPE']
33
+ content_type.nil? || content_type.empty? ? nil : content_type
34
+ end
35
+
33
36
  def session; @env['rack.session'] ||= {} end
34
37
  def session_options; @env['rack.session.options'] ||= {} end
35
38
  def logger; @env['rack.logger'] end
@@ -51,9 +54,9 @@ module Rack
51
54
  # { 'charset' => 'utf-8' }
52
55
  def media_type_params
53
56
  return {} if content_type.nil?
54
- content_type.split(/\s*[;,]\s*/)[1..-1].
57
+ Hash[*content_type.split(/\s*[;,]\s*/)[1..-1].
55
58
  collect { |s| s.split('=', 2) }.
56
- inject({}) { |hash,(k,v)| hash[k.downcase] = v ; hash }
59
+ map { |k,v| [k.downcase, v] }.flatten]
57
60
  end
58
61
 
59
62
  # The character set of the request body if a "charset" media type
@@ -64,6 +67,22 @@ module Rack
64
67
  media_type_params['charset']
65
68
  end
66
69
 
70
+ def scheme
71
+ if @env['HTTPS'] == 'on'
72
+ 'https'
73
+ elsif @env['HTTP_X_FORWARDED_SSL'] == 'on'
74
+ 'https'
75
+ elsif @env['HTTP_X_FORWARDED_PROTO']
76
+ @env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
77
+ else
78
+ @env["rack.url_scheme"]
79
+ end
80
+ end
81
+
82
+ def ssl?
83
+ scheme == 'https'
84
+ end
85
+
67
86
  def host_with_port
68
87
  if forwarded = @env["HTTP_X_FORWARDED_HOST"]
69
88
  forwarded.split(/,\s?/).last
@@ -72,6 +91,20 @@ module Rack
72
91
  end
73
92
  end
74
93
 
94
+ def port
95
+ if port = host_with_port.split(/:/)[1]
96
+ port.to_i
97
+ elsif port = @env['HTTP_X_FORWARDED_PORT']
98
+ port.to_i
99
+ elsif ssl?
100
+ 443
101
+ elsif @env.has_key?("HTTP_X_FORWARDED_HOST")
102
+ 80
103
+ else
104
+ @env["SERVER_PORT"].to_i
105
+ end
106
+ end
107
+
75
108
  def host
76
109
  # Remove port number.
77
110
  host_with_port.to_s.gsub(/:\d+\z/, '')
@@ -84,6 +117,7 @@ module Rack
84
117
  def get?; request_method == "GET" end
85
118
  def head?; request_method == "HEAD" end
86
119
  def options?; request_method == "OPTIONS" end
120
+ def patch?; request_method == "PATCH" end
87
121
  def post?; request_method == "POST" end
88
122
  def put?; request_method == "PUT" end
89
123
  def trace?; request_method == "TRACE" end
@@ -149,7 +183,8 @@ module Rack
149
183
  form_vars = @env["rack.input"].read
150
184
 
151
185
  # Fix for Safari Ajax postings that always append \0
152
- form_vars.sub!(/\0\z/, '')
186
+ # form_vars.sub!(/\0\z/, '') # performance replacement:
187
+ form_vars.slice!(-1) if form_vars[-1] == ?\0
153
188
 
154
189
  @env["rack.request.form_vars"] = form_vars
155
190
  @env["rack.request.form_hash"] = parse_query(form_vars)
@@ -164,8 +199,8 @@ module Rack
164
199
 
165
200
  # The union of GET and POST data.
166
201
  def params
167
- self.GET.update(self.POST)
168
- rescue EOFError => e
202
+ @params ||= self.GET.merge(self.POST)
203
+ rescue EOFError
169
204
  self.GET
170
205
  end
171
206
 
@@ -184,9 +219,9 @@ module Rack
184
219
  keys.map{|key| params[key] }
185
220
  end
186
221
 
187
- # the referer of the client or '/'
222
+ # the referer of the client
188
223
  def referer
189
- @env['HTTP_REFERER'] || '/'
224
+ @env['HTTP_REFERER']
190
225
  end
191
226
  alias referrer referer
192
227
 
@@ -207,10 +242,9 @@ module Rack
207
242
  # precede those with less specific. Ordering with respect to other
208
243
  # attributes (e.g., Domain) is unspecified.
209
244
  @env["rack.request.cookie_hash"] =
210
- Utils.parse_query(@env["rack.request.cookie_string"], ';,').inject({}) {|h,(k,v)|
211
- h[k] = Array === v ? v.first : v
212
- h
213
- }
245
+ Hash[*Utils.parse_query(@env["rack.request.cookie_string"], ';,').map {|k,v|
246
+ [k, Array === v ? v.first : v]
247
+ }.flatten]
214
248
  end
215
249
  end
216
250
 
@@ -218,8 +252,7 @@ module Rack
218
252
  @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
219
253
  end
220
254
 
221
- # Tries to return a remake of the original request URL as a string.
222
- def url
255
+ def base_url
223
256
  url = scheme + "://"
224
257
  url << host
225
258
 
@@ -228,11 +261,14 @@ module Rack
228
261
  url << ":#{port}"
229
262
  end
230
263
 
231
- url << fullpath
232
-
233
264
  url
234
265
  end
235
266
 
267
+ # Tries to return a remake of the original request URL as a string.
268
+ def url
269
+ base_url + fullpath
270
+ end
271
+
236
272
  def path
237
273
  script_name + path_info
238
274
  end
@@ -267,7 +303,7 @@ module Rack
267
303
  end
268
304
 
269
305
  def parse_multipart(env)
270
- Utils::Multipart.parse_multipart(env)
306
+ Rack::Multipart.parse_multipart(env)
271
307
  end
272
308
  end
273
309
  end
@@ -21,12 +21,13 @@ module Rack
21
21
 
22
22
  def initialize(body=[], status=200, header={}, &block)
23
23
  @status = status.to_i
24
- @header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
25
- merge(header))
24
+ @header = Utils::HeaderHash.new("Content-Type" => "text/html").
25
+ merge(header)
26
26
 
27
- @writer = lambda { |x| @body << x }
28
- @block = nil
29
- @length = 0
27
+ @chunked = "chunked" == @header['Transfer-Encoding']
28
+ @writer = lambda { |x| @body << x }
29
+ @block = nil
30
+ @length = 0
30
31
 
31
32
  @body = []
32
33
 
@@ -72,12 +73,14 @@ module Rack
72
73
 
73
74
  if [204, 304].include?(status.to_i)
74
75
  header.delete "Content-Type"
76
+ header.delete "Content-Length"
75
77
  [status.to_i, header, []]
76
78
  else
77
79
  [status.to_i, header, self]
78
80
  end
79
81
  end
80
82
  alias to_a finish # For *response
83
+ alias to_ary finish # For implicit-splat on Ruby 1.9.2
81
84
 
82
85
  def each(&callback)
83
86
  @body.each(&callback)
@@ -91,10 +94,10 @@ module Rack
91
94
  #
92
95
  def write(str)
93
96
  s = str.to_s
94
- @length += Rack::Utils.bytesize(s)
97
+ @length += Rack::Utils.bytesize(s) unless @chunked
95
98
  @writer.call s
96
99
 
97
- header["Content-Length"] = @length.to_s
100
+ header["Content-Length"] = @length.to_s unless @chunked
98
101
  str
99
102
  end
100
103
 
@@ -122,7 +125,6 @@ module Rack
122
125
  def not_found?; @status == 404; end
123
126
 
124
127
  def redirect?; [301, 302, 303, 307].include? @status; end
125
- def empty?; [201, 204, 304].include? @status; end
126
128
 
127
129
  # Headers
128
130
  attr_reader :headers, :original_headers
@@ -1,11 +1,6 @@
1
1
  require 'rack/file'
2
2
 
3
3
  module Rack
4
- class File #:nodoc:
5
- unless instance_methods(false).include?('to_path')
6
- alias :to_path :path
7
- end
8
- end
9
4
 
10
5
  # = Sendfile
11
6
  #
@@ -52,9 +47,10 @@ module Rack
52
47
  # }
53
48
  #
54
49
  # Note that the X-Sendfile-Type header must be set exactly as shown above. The
55
- # X-Accel-Mapping header should specify the internal URI path, followed by an
56
- # equals sign (=), followed name of the location in the file system that it maps
57
- # to. The middleware performs a simple substitution on the resulting path.
50
+ # X-Accel-Mapping header should specify the by the location on the file system,
51
+ # followed by an equals sign (=), followed name of the private URL pattern
52
+ # that it maps to. The middleware performs a simple substitution on the
53
+ # resulting path.
58
54
  #
59
55
  # See Also: http://wiki.codemongers.com/NginxXSendfile
60
56
  #
@@ -128,17 +124,17 @@ module Rack
128
124
  end
129
125
 
130
126
  private
131
- def variation(env)
132
- @variation ||
133
- env['sendfile.type'] ||
134
- env['HTTP_X_SENDFILE_TYPE']
135
- end
127
+ def variation(env)
128
+ @variation ||
129
+ env['sendfile.type'] ||
130
+ env['HTTP_X_SENDFILE_TYPE']
131
+ end
136
132
 
137
- def map_accel_path(env, file)
138
- if mapping = env['HTTP_X_ACCEL_MAPPING']
139
- internal, external = mapping.split('=', 2).map{ |p| p.strip }
140
- file.sub(/^#{internal}/i, external)
141
- end
133
+ def map_accel_path(env, file)
134
+ if mapping = env['HTTP_X_ACCEL_MAPPING']
135
+ internal, external = mapping.split('=', 2).map{ |p| p.strip }
136
+ file.sub(/^#{internal}/i, external)
142
137
  end
138
+ end
143
139
  end
144
140
  end