rack 0.9.1 → 1.0.0

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 (79) hide show
  1. data/COPYING +1 -1
  2. data/RDOX +115 -16
  3. data/README +54 -7
  4. data/Rakefile +61 -85
  5. data/SPEC +50 -17
  6. data/bin/rackup +9 -5
  7. data/example/protectedlobster.ru +1 -1
  8. data/lib/rack.rb +7 -3
  9. data/lib/rack/auth/abstract/handler.rb +13 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -1
  11. data/lib/rack/auth/digest/request.rb +2 -2
  12. data/lib/rack/auth/openid.rb +344 -302
  13. data/lib/rack/builder.rb +1 -5
  14. data/lib/rack/chunked.rb +49 -0
  15. data/lib/rack/conditionalget.rb +4 -0
  16. data/lib/rack/content_length.rb +7 -3
  17. data/lib/rack/content_type.rb +23 -0
  18. data/lib/rack/deflater.rb +83 -74
  19. data/lib/rack/directory.rb +5 -2
  20. data/lib/rack/file.rb +4 -1
  21. data/lib/rack/handler.rb +22 -1
  22. data/lib/rack/handler/cgi.rb +7 -3
  23. data/lib/rack/handler/fastcgi.rb +26 -24
  24. data/lib/rack/handler/lsws.rb +7 -4
  25. data/lib/rack/handler/mongrel.rb +5 -3
  26. data/lib/rack/handler/scgi.rb +5 -3
  27. data/lib/rack/handler/thin.rb +3 -0
  28. data/lib/rack/handler/webrick.rb +11 -5
  29. data/lib/rack/lint.rb +138 -66
  30. data/lib/rack/lock.rb +16 -0
  31. data/lib/rack/mime.rb +4 -4
  32. data/lib/rack/mock.rb +3 -3
  33. data/lib/rack/reloader.rb +88 -46
  34. data/lib/rack/request.rb +46 -10
  35. data/lib/rack/response.rb +15 -3
  36. data/lib/rack/rewindable_input.rb +98 -0
  37. data/lib/rack/session/abstract/id.rb +71 -82
  38. data/lib/rack/session/cookie.rb +2 -0
  39. data/lib/rack/session/memcache.rb +59 -47
  40. data/lib/rack/session/pool.rb +56 -29
  41. data/lib/rack/showexceptions.rb +2 -1
  42. data/lib/rack/showstatus.rb +1 -1
  43. data/lib/rack/urlmap.rb +12 -5
  44. data/lib/rack/utils.rb +115 -65
  45. data/rack.gemspec +54 -0
  46. data/test/multipart/binary +0 -0
  47. data/test/multipart/empty +10 -0
  48. data/test/multipart/ie +6 -0
  49. data/test/multipart/nested +10 -0
  50. data/test/multipart/none +9 -0
  51. data/test/multipart/text +10 -0
  52. data/test/spec_rack_auth_basic.rb +5 -1
  53. data/test/spec_rack_auth_digest.rb +93 -36
  54. data/test/spec_rack_auth_openid.rb +47 -100
  55. data/test/spec_rack_builder.rb +2 -2
  56. data/test/spec_rack_chunked.rb +62 -0
  57. data/test/spec_rack_conditionalget.rb +7 -7
  58. data/test/spec_rack_content_type.rb +30 -0
  59. data/test/spec_rack_deflater.rb +36 -14
  60. data/test/spec_rack_directory.rb +1 -1
  61. data/test/spec_rack_file.rb +11 -0
  62. data/test/spec_rack_handler.rb +21 -2
  63. data/test/spec_rack_lint.rb +163 -44
  64. data/test/spec_rack_lock.rb +38 -0
  65. data/test/spec_rack_mock.rb +6 -1
  66. data/test/spec_rack_request.rb +81 -12
  67. data/test/spec_rack_response.rb +46 -2
  68. data/test/spec_rack_rewindable_input.rb +118 -0
  69. data/test/spec_rack_session_memcache.rb +170 -62
  70. data/test/spec_rack_session_pool.rb +129 -41
  71. data/test/spec_rack_static.rb +2 -2
  72. data/test/spec_rack_thin.rb +3 -2
  73. data/test/spec_rack_urlmap.rb +10 -0
  74. data/test/spec_rack_utils.rb +214 -49
  75. data/test/spec_rack_webrick.rb +7 -0
  76. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  77. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  78. metadata +95 -6
  79. data/AUTHORS +0 -8
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ class Lock
3
+ FLAG = 'rack.multithread'.freeze
4
+
5
+ def initialize(app, lock = Mutex.new)
6
+ @app, @lock = app, lock
7
+ end
8
+
9
+ def call(env)
10
+ old, env[FLAG] = env[FLAG], false
11
+ @lock.synchronize { @app.call(env) }
12
+ ensure
13
+ env[FLAG] = old
14
+ end
15
+ end
16
+ end
@@ -8,10 +8,10 @@ module Rack
8
8
  # Also see the documentation for MIME_TYPES
9
9
  #
10
10
  # Usage:
11
- # Rack::Utils.mime_type('.foo')
11
+ # Rack::Mime.mime_type('.foo')
12
12
  #
13
13
  # This is a shortcut for:
14
- # Rack::Utils::MIME_TYPES.fetch('.foo', 'application/octet-stream')
14
+ # Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
15
15
 
16
16
  def mime_type(ext, fallback='application/octet-stream')
17
17
  MIME_TYPES.fetch(ext, fallback)
@@ -26,12 +26,12 @@ module Rack
26
26
  #
27
27
  # require 'webrick/httputils'
28
28
  # list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
29
- # Rack::Utils::MIME_TYPES.merge!(list)
29
+ # Rack::Mime::MIME_TYPES.merge!(list)
30
30
  #
31
31
  # To add the list mongrel provides, use:
32
32
  #
33
33
  # require 'mongrel/handlers'
34
- # Rack::Utils::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
34
+ # Rack::Mime::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
35
35
 
36
36
  MIME_TYPES = {
37
37
  ".3gp" => "video/3gpp",
@@ -97,6 +97,8 @@ module Rack
97
97
  env["rack.input"] = opts[:input]
98
98
  end
99
99
 
100
+ env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
101
+
100
102
  opts.each { |field, value|
101
103
  env[field] = value if String === field
102
104
  }
@@ -116,9 +118,7 @@ module Rack
116
118
  @original_headers = headers
117
119
  @headers = Rack::Utils::HeaderHash.new
118
120
  headers.each { |field, values|
119
- values.each { |value|
120
- @headers[field] = value
121
- }
121
+ @headers[field] = values
122
122
  @headers[field] = "" if values.empty?
123
123
  }
124
124
 
@@ -1,64 +1,106 @@
1
- require 'thread'
1
+ # Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
2
+ # All files in this distribution are subject to the terms of the Ruby license.
3
+
4
+ require 'pathname'
2
5
 
3
6
  module Rack
4
- # Rack::Reloader checks on every request, but at most every +secs+
5
- # seconds, if a file loaded changed, and reloads it, logging to
6
- # rack.errors.
7
- #
8
- # It is recommended you use ShowExceptions to catch SyntaxErrors etc.
9
7
 
8
+ # High performant source reloader
9
+ #
10
+ # This class acts as Rack middleware.
11
+ #
12
+ # What makes it especially suited for use in a production environment is that
13
+ # any file will only be checked once and there will only be made one system
14
+ # call stat(2).
15
+ #
16
+ # Please note that this will not reload files in the background, it does so
17
+ # only when actively called.
18
+ #
19
+ # It is performing a check/reload cycle at the start of every request, but
20
+ # also respects a cool down time, during which nothing will be done.
10
21
  class Reloader
11
- def initialize(app, secs=10)
22
+ def initialize(app, cooldown = 10, backend = Stat)
12
23
  @app = app
13
- @secs = secs # reload every @secs seconds max
14
- @last = Time.now
24
+ @cooldown = cooldown
25
+ @last = (Time.now - cooldown)
26
+ @cache = {}
27
+ @mtimes = {}
28
+
29
+ extend backend
15
30
  end
16
31
 
17
32
  def call(env)
18
- if Time.now > @last + @secs
19
- Thread.exclusive {
20
- reload!(env['rack.errors'])
21
- @last = Time.now
22
- }
33
+ if @cooldown and Time.now > @last + @cooldown
34
+ if Thread.list.size > 1
35
+ Thread.exclusive{ reload! }
36
+ else
37
+ reload!
38
+ end
39
+
40
+ @last = Time.now
23
41
  end
24
42
 
25
43
  @app.call(env)
26
44
  end
27
45
 
28
- def reload!(stderr=STDERR)
29
- need_reload = $LOADED_FEATURES.find_all { |loaded|
30
- begin
31
- if loaded =~ /\A[.\/]/ # absolute filename or 1.9
32
- abs = loaded
33
- else
34
- abs = $LOAD_PATH.map { |path| ::File.join(path, loaded) }.
35
- find { |file| ::File.exist? file }
36
- end
37
-
38
- if abs
39
- ::File.mtime(abs) > @last - @secs rescue false
40
- else
41
- false
42
- end
43
- end
44
- }
45
-
46
- need_reload.each { |l|
47
- $LOADED_FEATURES.delete l
48
- }
49
-
50
- need_reload.each { |to_load|
51
- begin
52
- if require to_load
53
- stderr.puts "#{self.class}: reloaded `#{to_load}'"
54
- end
55
- rescue LoadError, SyntaxError => e
56
- raise e # Possibly ShowExceptions
46
+ def reload!(stderr = $stderr)
47
+ rotation do |file, mtime|
48
+ previous_mtime = @mtimes[file] ||= mtime
49
+ safe_load(file, mtime, stderr) if mtime > previous_mtime
50
+ end
51
+ end
52
+
53
+ # A safe Kernel::load, issuing the hooks depending on the results
54
+ def safe_load(file, mtime, stderr = $stderr)
55
+ load(file)
56
+ stderr.puts "#{self.class}: reloaded `#{file}'"
57
+ file
58
+ rescue LoadError, SyntaxError => ex
59
+ stderr.puts ex
60
+ ensure
61
+ @mtimes[file] = mtime
62
+ end
63
+
64
+ module Stat
65
+ def rotation
66
+ files = [$0, *$LOADED_FEATURES].uniq
67
+ paths = ['./', *$LOAD_PATH].uniq
68
+
69
+ files.map{|file|
70
+ next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
71
+
72
+ found, stat = figure_path(file, paths)
73
+ next unless found and stat and mtime = stat.mtime
74
+
75
+ @cache[file] = found
76
+
77
+ yield(found, mtime)
78
+ }.compact
79
+ end
80
+
81
+ # Takes a relative or absolute +file+ name, a couple possible +paths+ that
82
+ # the +file+ might reside in. Returns the full path and File::Stat for the
83
+ # path.
84
+ def figure_path(file, paths)
85
+ found = @cache[file]
86
+ found = file if !found and Pathname.new(file).absolute?
87
+ found, stat = safe_stat(found)
88
+ return found, stat if found
89
+
90
+ paths.each do |possible_path|
91
+ path = ::File.join(possible_path, file)
92
+ found, stat = safe_stat(path)
93
+ return ::File.expand_path(found), stat if found
57
94
  end
58
- }
95
+ end
59
96
 
60
- stderr.flush
61
- need_reload
97
+ def safe_stat(file)
98
+ return unless file
99
+ stat = ::File.stat(file)
100
+ return file, stat if stat.file?
101
+ rescue Errno::ENOENT, Errno::ENOTDIR
102
+ @cache.delete(file) and false
103
+ end
62
104
  end
63
105
  end
64
106
  end
@@ -8,11 +8,23 @@ module Rack
8
8
  # req = Rack::Request.new(env)
9
9
  # req.post?
10
10
  # req.params["data"]
11
+ #
12
+ # The environment hash passed will store a reference to the Request object
13
+ # instantiated so that it will only instantiate if an instance of the Request
14
+ # object doesn't already exist.
11
15
 
12
16
  class Request
13
17
  # The environment of the request.
14
18
  attr_reader :env
15
19
 
20
+ def self.new(env, *args)
21
+ if self == Rack::Request
22
+ env["rack.request"] ||= super
23
+ else
24
+ super
25
+ end
26
+ end
27
+
16
28
  def initialize(env)
17
29
  @env = env
18
30
  end
@@ -26,6 +38,8 @@ module Rack
26
38
  def query_string; @env["QUERY_STRING"].to_s end
27
39
  def content_length; @env['CONTENT_LENGTH'] end
28
40
  def content_type; @env['CONTENT_TYPE'] end
41
+ def session; @env['rack.session'] ||= {} end
42
+ def session_options; @env['rack.session.options'] ||= {} end
29
43
 
30
44
  # The media type (type/subtype) portion of the CONTENT_TYPE header
31
45
  # without any media type parameters. e.g., when CONTENT_TYPE is
@@ -34,7 +48,7 @@ module Rack
34
48
  # For more information on the use of media types in HTTP, see:
35
49
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
36
50
  def media_type
37
- content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase
51
+ content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
38
52
  end
39
53
 
40
54
  # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -80,6 +94,14 @@ module Rack
80
94
  'multipart/form-data'
81
95
  ]
82
96
 
97
+ # The set of media-types. Requests that do not indicate
98
+ # one of the media types presents in this list will not be eligible
99
+ # for param parsing like soap attachments or generic multiparts
100
+ PARSEABLE_DATA_MEDIA_TYPES = [
101
+ 'multipart/related',
102
+ 'multipart/mixed'
103
+ ]
104
+
83
105
  # Determine whether the request body contains form-data by checking
84
106
  # the request media_type against registered form-data media-types:
85
107
  # "application/x-www-form-urlencoded" and "multipart/form-data". The
@@ -89,6 +111,12 @@ module Rack
89
111
  FORM_DATA_MEDIA_TYPES.include?(media_type)
90
112
  end
91
113
 
114
+ # Determine whether the request body contains data by checking
115
+ # the request media_type against registered parse-data media-types
116
+ def parseable_data?
117
+ PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
118
+ end
119
+
92
120
  # Returns the data recieved in the query string.
93
121
  def GET
94
122
  if @env["rack.request.query_string"] == query_string
@@ -96,7 +124,7 @@ module Rack
96
124
  else
97
125
  @env["rack.request.query_string"] = query_string
98
126
  @env["rack.request.query_hash"] =
99
- Utils.parse_query(query_string)
127
+ Utils.parse_nested_query(query_string)
100
128
  end
101
129
  end
102
130
 
@@ -107,13 +135,19 @@ module Rack
107
135
  def POST
108
136
  if @env["rack.request.form_input"].eql? @env["rack.input"]
109
137
  @env["rack.request.form_hash"]
110
- elsif form_data?
138
+ elsif form_data? || parseable_data?
111
139
  @env["rack.request.form_input"] = @env["rack.input"]
112
140
  unless @env["rack.request.form_hash"] =
113
141
  Utils::Multipart.parse_multipart(env)
114
- @env["rack.request.form_vars"] = @env["rack.input"].read
115
- @env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
116
- @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
142
+ form_vars = @env["rack.input"].read
143
+
144
+ # Fix for Safari Ajax postings that always append \0
145
+ form_vars.sub!(/\0\z/, '')
146
+
147
+ @env["rack.request.form_vars"] = form_vars
148
+ @env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars)
149
+
150
+ @env["rack.input"].rewind
117
151
  end
118
152
  @env["rack.request.form_hash"]
119
153
  else
@@ -188,11 +222,13 @@ module Rack
188
222
 
189
223
  url
190
224
  end
191
-
225
+
226
+ def path
227
+ script_name + path_info
228
+ end
229
+
192
230
  def fullpath
193
- path = script_name + path_info
194
- path << "?" << query_string unless query_string.empty?
195
- path
231
+ query_string.empty? ? path : "#{path}?#{query_string}"
196
232
  end
197
233
 
198
234
  def accept_encoding
@@ -16,6 +16,8 @@ module Rack
16
16
  # Your application's +call+ should end returning Response#finish.
17
17
 
18
18
  class Response
19
+ attr_accessor :length
20
+
19
21
  def initialize(body=[], status=200, header={}, &block)
20
22
  @status = status
21
23
  @header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
@@ -61,12 +63,13 @@ module Rack
61
63
  expires = "; expires=" + value[:expires].clone.gmtime.
62
64
  strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
63
65
  secure = "; secure" if value[:secure]
66
+ httponly = "; HttpOnly" if value[:httponly]
64
67
  value = value[:value]
65
68
  end
66
69
  value = [value] unless Array === value
67
70
  cookie = Utils.escape(key) + "=" +
68
71
  value.map { |v| Utils.escape v }.join("&") +
69
- "#{domain}#{path}#{expires}#{secure}"
72
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
70
73
 
71
74
  case self["Set-Cookie"]
72
75
  when Array
@@ -92,6 +95,10 @@ module Rack
92
95
  :expires => Time.at(0) }.merge(value))
93
96
  end
94
97
 
98
+ def redirect(target, status=302)
99
+ self.status = status
100
+ self["Location"] = target
101
+ end
95
102
 
96
103
  def finish(&block)
97
104
  @block = block
@@ -100,7 +107,6 @@ module Rack
100
107
  header.delete "Content-Type"
101
108
  [status.to_i, header.to_hash, []]
102
109
  else
103
- header["Content-Length"] ||= @length.to_s
104
110
  [status.to_i, header.to_hash, self]
105
111
  end
106
112
  end
@@ -112,10 +118,16 @@ module Rack
112
118
  @block.call(self) if @block
113
119
  end
114
120
 
121
+ # Append to body and update Content-Length.
122
+ #
123
+ # NOTE: Do not mix #write and direct #body access!
124
+ #
115
125
  def write(str)
116
126
  s = str.to_s
117
- @length += s.size
127
+ @length += Rack::Utils.bytesize(s)
118
128
  @writer.call s
129
+
130
+ header["Content-Length"] = @length.to_s
119
131
  str
120
132
  end
121
133
 
@@ -0,0 +1,98 @@
1
+ require 'tempfile'
2
+
3
+ module Rack
4
+ # Class which can make any IO object rewindable, including non-rewindable ones. It does
5
+ # this by buffering the data into a tempfile, which is rewindable.
6
+ #
7
+ # rack.input is required to be rewindable, so if your input stream IO is non-rewindable
8
+ # by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
9
+ # to easily make it rewindable.
10
+ #
11
+ # Don't forget to call #close when you're done. This frees up temporary resources that
12
+ # RewindableInput uses, though it does *not* close the original IO object.
13
+ class RewindableInput
14
+ def initialize(io)
15
+ @io = io
16
+ @rewindable_io = nil
17
+ @unlinked = false
18
+ end
19
+
20
+ def gets
21
+ make_rewindable unless @rewindable_io
22
+ @rewindable_io.gets
23
+ end
24
+
25
+ def read(*args)
26
+ make_rewindable unless @rewindable_io
27
+ @rewindable_io.read(*args)
28
+ end
29
+
30
+ def each(&block)
31
+ make_rewindable unless @rewindable_io
32
+ @rewindable_io.each(&block)
33
+ end
34
+
35
+ def rewind
36
+ make_rewindable unless @rewindable_io
37
+ @rewindable_io.rewind
38
+ end
39
+
40
+ # Closes this RewindableInput object without closing the originally
41
+ # wrapped IO oject. Cleans up any temporary resources that this RewindableInput
42
+ # has created.
43
+ #
44
+ # This method may be called multiple times. It does nothing on subsequent calls.
45
+ def close
46
+ if @rewindable_io
47
+ if @unlinked
48
+ @rewindable_io.close
49
+ else
50
+ @rewindable_io.close!
51
+ end
52
+ @rewindable_io = nil
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Ruby's Tempfile class has a bug. Subclass it and fix it.
59
+ class Tempfile < ::Tempfile
60
+ def _close
61
+ @tmpfile.close if @tmpfile
62
+ @data[1] = nil if @data
63
+ @tmpfile = nil
64
+ end
65
+ end
66
+
67
+ def make_rewindable
68
+ # Buffer all data into a tempfile. Since this tempfile is private to this
69
+ # RewindableInput object, we chmod it so that nobody else can read or write
70
+ # it. On POSIX filesystems we also unlink the file so that it doesn't
71
+ # even have a file entry on the filesystem anymore, though we can still
72
+ # access it because we have the file handle open.
73
+ @rewindable_io = Tempfile.new('RackRewindableInput')
74
+ @rewindable_io.chmod(0000)
75
+ if filesystem_has_posix_semantics?
76
+ @rewindable_io.unlink
77
+ @unlinked = true
78
+ end
79
+
80
+ buffer = ""
81
+ while @io.read(1024 * 4, buffer)
82
+ entire_buffer_written_out = false
83
+ while !entire_buffer_written_out
84
+ written = @rewindable_io.write(buffer)
85
+ entire_buffer_written_out = written == buffer.size
86
+ if !entire_buffer_written_out
87
+ buffer.slice!(0 .. written - 1)
88
+ end
89
+ end
90
+ end
91
+ @rewindable_io.rewind
92
+ end
93
+
94
+ def filesystem_has_posix_semantics?
95
+ RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
96
+ end
97
+ end
98
+ end