rack 1.0.1 → 1.1.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 (82) hide show
  1. data/COPYING +1 -1
  2. data/KNOWN-ISSUES +3 -0
  3. data/RDOX +0 -428
  4. data/README +61 -26
  5. data/SPEC +8 -1
  6. data/bin/rackup +2 -174
  7. data/lib/rack.rb +10 -8
  8. data/lib/rack/builder.rb +17 -0
  9. data/lib/rack/cascade.rb +17 -12
  10. data/lib/rack/chunked.rb +2 -2
  11. data/lib/rack/commonlogger.rb +31 -43
  12. data/lib/rack/config.rb +15 -0
  13. data/lib/rack/content_type.rb +1 -1
  14. data/lib/rack/directory.rb +6 -2
  15. data/lib/rack/etag.rb +23 -0
  16. data/lib/rack/file.rb +4 -2
  17. data/lib/rack/handler.rb +19 -0
  18. data/lib/rack/handler/cgi.rb +1 -1
  19. data/lib/rack/handler/fastcgi.rb +2 -3
  20. data/lib/rack/handler/lsws.rb +4 -1
  21. data/lib/rack/handler/mongrel.rb +8 -5
  22. data/lib/rack/handler/scgi.rb +4 -4
  23. data/lib/rack/handler/webrick.rb +2 -4
  24. data/lib/rack/lint.rb +44 -15
  25. data/lib/rack/logger.rb +20 -0
  26. data/lib/rack/mime.rb +3 -1
  27. data/lib/rack/mock.rb +30 -4
  28. data/lib/rack/nulllogger.rb +18 -0
  29. data/lib/rack/reloader.rb +4 -1
  30. data/lib/rack/request.rb +40 -15
  31. data/lib/rack/response.rb +5 -39
  32. data/lib/rack/runtime.rb +27 -0
  33. data/lib/rack/sendfile.rb +142 -0
  34. data/lib/rack/server.rb +212 -0
  35. data/lib/rack/session/abstract/id.rb +3 -5
  36. data/lib/rack/session/cookie.rb +3 -4
  37. data/lib/rack/session/memcache.rb +53 -43
  38. data/lib/rack/session/pool.rb +1 -1
  39. data/lib/rack/urlmap.rb +9 -8
  40. data/lib/rack/utils.rb +230 -11
  41. data/rack.gemspec +33 -49
  42. data/test/spec_rack_cascade.rb +3 -5
  43. data/test/spec_rack_cgi.rb +3 -3
  44. data/test/spec_rack_commonlogger.rb +39 -10
  45. data/test/spec_rack_config.rb +24 -0
  46. data/test/spec_rack_directory.rb +1 -1
  47. data/test/spec_rack_etag.rb +17 -0
  48. data/test/spec_rack_fastcgi.rb +2 -2
  49. data/test/spec_rack_file.rb +1 -1
  50. data/test/spec_rack_lint.rb +26 -19
  51. data/test/spec_rack_logger.rb +21 -0
  52. data/test/spec_rack_mock.rb +87 -1
  53. data/test/spec_rack_mongrel.rb +4 -4
  54. data/test/spec_rack_nulllogger.rb +13 -0
  55. data/test/spec_rack_request.rb +47 -6
  56. data/test/spec_rack_response.rb +3 -0
  57. data/test/spec_rack_runtime.rb +35 -0
  58. data/test/spec_rack_sendfile.rb +86 -0
  59. data/test/spec_rack_session_cookie.rb +1 -10
  60. data/test/spec_rack_session_memcache.rb +53 -20
  61. data/test/spec_rack_urlmap.rb +30 -0
  62. data/test/spec_rack_utils.rb +171 -6
  63. data/test/spec_rack_webrick.rb +4 -4
  64. data/test/spec_rackup.rb +154 -0
  65. metadata +37 -79
  66. data/Rakefile +0 -164
  67. data/lib/rack/auth/openid.rb +0 -480
  68. data/test/cgi/lighttpd.conf +0 -20
  69. data/test/cgi/test +0 -9
  70. data/test/cgi/test.fcgi +0 -8
  71. data/test/cgi/test.ru +0 -7
  72. data/test/multipart/binary +0 -0
  73. data/test/multipart/empty +0 -10
  74. data/test/multipart/ie +0 -6
  75. data/test/multipart/nested +0 -10
  76. data/test/multipart/none +0 -9
  77. data/test/multipart/semicolon +0 -6
  78. data/test/multipart/text +0 -10
  79. data/test/spec_rack_auth_openid.rb +0 -84
  80. data/test/testrequest.rb +0 -57
  81. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  82. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
@@ -0,0 +1,212 @@
1
+ require 'optparse'
2
+
3
+ module Rack
4
+ class Server
5
+ class Options
6
+ def parse!(args)
7
+ options = {}
8
+ opt_parser = OptionParser.new("", 24, ' ') do |opts|
9
+ opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
10
+
11
+ opts.separator ""
12
+ opts.separator "Ruby options:"
13
+
14
+ lineno = 1
15
+ opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
16
+ eval line, TOPLEVEL_BINDING, "-e", lineno
17
+ lineno += 1
18
+ }
19
+
20
+ opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
21
+ options[:debug] = true
22
+ }
23
+ opts.on("-w", "--warn", "turn warnings on for your script") {
24
+ options[:warn] = true
25
+ }
26
+
27
+ opts.on("-I", "--include PATH",
28
+ "specify $LOAD_PATH (may be used more than once)") { |path|
29
+ options[:include] = path.split(":")
30
+ }
31
+
32
+ opts.on("-r", "--require LIBRARY",
33
+ "require the library, before executing your script") { |library|
34
+ options[:require] = library
35
+ }
36
+
37
+ opts.separator ""
38
+ opts.separator "Rack options:"
39
+ opts.on("-s", "--server SERVER", "serve using SERVER (webrick/mongrel)") { |s|
40
+ options[:server] = s
41
+ }
42
+
43
+ opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") { |host|
44
+ options[:Host] = host
45
+ }
46
+
47
+ opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
48
+ options[:Port] = port
49
+ }
50
+
51
+ opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
52
+ options[:environment] = e
53
+ }
54
+
55
+ opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
56
+ options[:daemonize] = d ? true : false
57
+ }
58
+
59
+ opts.on("-P", "--pid FILE", "file to store PID (default: rack.pid)") { |f|
60
+ options[:pid] = f
61
+ }
62
+
63
+ opts.separator ""
64
+ opts.separator "Common options:"
65
+
66
+ opts.on_tail("-h", "--help", "Show this message") do
67
+ puts opts
68
+ exit
69
+ end
70
+
71
+ opts.on_tail("--version", "Show version") do
72
+ puts "Rack #{Rack.version}"
73
+ exit
74
+ end
75
+ end
76
+ opt_parser.parse! args
77
+ options[:config] = args.last if args.last
78
+ options
79
+ end
80
+ end
81
+
82
+ def self.start
83
+ new.start
84
+ end
85
+
86
+ attr_accessor :options
87
+
88
+ def initialize(options = nil)
89
+ @options = options
90
+ end
91
+
92
+ def options
93
+ @options ||= parse_options(ARGV)
94
+ end
95
+
96
+ def default_options
97
+ {
98
+ :environment => "development",
99
+ :pid => nil,
100
+ :Port => 9292,
101
+ :Host => "0.0.0.0",
102
+ :AccessLog => [],
103
+ :config => "config.ru"
104
+ }
105
+ end
106
+
107
+ def app
108
+ @app ||= begin
109
+ if !::File.exist? options[:config]
110
+ abort "configuration #{options[:config]} not found"
111
+ end
112
+
113
+ app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
114
+ self.options.merge! options
115
+ app
116
+ end
117
+ end
118
+
119
+ def self.middleware
120
+ @middleware ||= begin
121
+ m = Hash.new {|h,k| h[k] = []}
122
+ m["deployment"].concat [lambda {|server| server.server =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] }]
123
+ m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]]
124
+ m
125
+ end
126
+ end
127
+
128
+ def middleware
129
+ self.class.middleware
130
+ end
131
+
132
+ def start
133
+ if options[:debug]
134
+ $DEBUG = true
135
+ require 'pp'
136
+ p options[:server]
137
+ pp wrapped_app
138
+ pp app
139
+ end
140
+
141
+ if options[:warn]
142
+ $-w = true
143
+ end
144
+
145
+ if includes = options[:include]
146
+ $LOAD_PATH.unshift *includes
147
+ end
148
+
149
+ if library = options[:require]
150
+ require library
151
+ end
152
+
153
+ daemonize_app if options[:daemonize]
154
+ write_pid if options[:pid]
155
+ server.run wrapped_app, options
156
+ end
157
+
158
+ def server
159
+ @_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default
160
+ end
161
+
162
+ private
163
+ def parse_options(args)
164
+ options = default_options
165
+
166
+ # Don't evaluate CGI ISINDEX parameters.
167
+ # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
168
+ args.clear if ENV.include?("REQUEST_METHOD")
169
+
170
+ options.merge! opt_parser.parse! args
171
+ options
172
+ end
173
+
174
+ def opt_parser
175
+ Options.new
176
+ end
177
+
178
+ def build_app(app)
179
+ middleware[options[:environment]].reverse_each do |middleware|
180
+ middleware = middleware.call(self) if middleware.respond_to?(:call)
181
+ next unless middleware
182
+ klass = middleware.shift
183
+ app = klass.new(app, *middleware)
184
+ end
185
+ app
186
+ end
187
+
188
+ def wrapped_app
189
+ @wrapped_app ||= build_app app
190
+ end
191
+
192
+ def daemonize_app
193
+ if RUBY_VERSION < "1.9"
194
+ exit if fork
195
+ Process.setsid
196
+ exit if fork
197
+ Dir.chdir "/"
198
+ ::File.umask 0000
199
+ STDIN.reopen "/dev/null"
200
+ STDOUT.reopen "/dev/null", "a"
201
+ STDERR.reopen "/dev/null", "a"
202
+ else
203
+ Process.daemon
204
+ end
205
+ end
206
+
207
+ def write_pid
208
+ ::File.open(options[:pid], 'w'){ |f| f.write("#{Process.pid}") }
209
+ at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
210
+ end
211
+ end
212
+ end
@@ -107,18 +107,16 @@ module Rack
107
107
 
108
108
  if not session_id = set_session(env, session_id, session, options)
109
109
  env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
110
- [status, headers, body]
111
110
  elsif options[:defer] and not options[:renew]
112
111
  env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
113
- [status, headers, body]
114
112
  else
115
113
  cookie = Hash.new
116
114
  cookie[:value] = session_id
117
115
  cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
118
- response = Rack::Response.new(body, status, headers)
119
- response.set_cookie(@key, cookie.merge(options))
120
- response.to_a
116
+ Utils.set_cookie_header!(headers, @key, cookie.merge(options))
121
117
  end
118
+
119
+ [status, headers, body]
122
120
  end
123
121
 
124
122
  # All thread safety and session retrival proceedures should occur here.
@@ -70,16 +70,15 @@ module Rack
70
70
 
71
71
  if session_data.size > (4096 - @key.size)
72
72
  env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
73
- [status, headers, body]
74
73
  else
75
74
  options = env["rack.session.options"]
76
75
  cookie = Hash.new
77
76
  cookie[:value] = session_data
78
77
  cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
79
- response = Rack::Response.new(body, status, headers)
80
- response.set_cookie(@key, cookie.merge(options))
81
- response.to_a
78
+ Utils.set_cookie_header!(headers, @key, cookie.merge(options))
82
79
  end
80
+
81
+ [status, headers, body]
83
82
  end
84
83
 
85
84
  def generate_hmac(data)
@@ -29,9 +29,13 @@ module Rack
29
29
  super
30
30
 
31
31
  @mutex = Mutex.new
32
- @pool = MemCache.
33
- new @default_options[:memcache_server], @default_options
34
- raise 'No memcache servers' unless @pool.servers.any?{|s|s.alive?}
32
+ mserv = @default_options[:memcache_server]
33
+ mopts = @default_options.
34
+ reject{|k,v| MemCache::DEFAULT_OPTIONS.include? k }
35
+ @pool = MemCache.new mserv, mopts
36
+ unless @pool.active? and @pool.servers.any?{|c| c.alive? }
37
+ raise 'No memcache servers'
38
+ end
35
39
  end
36
40
 
37
41
  def generate_sid
@@ -41,24 +45,23 @@ module Rack
41
45
  end
42
46
  end
43
47
 
44
- def get_session(env, sid)
45
- session = @pool.get(sid) if sid
48
+ def get_session(env, session_id)
46
49
  @mutex.lock if env['rack.multithread']
47
- unless sid and session
48
- env['rack.errors'].puts("Session '#{sid.inspect}' not found, initializing...") if $VERBOSE and not sid.nil?
49
- session = {}
50
- sid = generate_sid
51
- ret = @pool.add sid, session
52
- raise "Session collision on '#{sid.inspect}'" unless /^STORED/ =~ ret
50
+ unless session_id and session = @pool.get(session_id)
51
+ session_id, session = generate_sid, {}
52
+ unless /^STORED/ =~ @pool.add(session_id, session)
53
+ raise "Session collision on '#{session_id.inspect}'"
54
+ end
53
55
  end
54
- session.instance_variable_set('@old', {}.merge(session))
55
- return [sid, session]
56
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
57
- warn "#{self} is unable to find server."
56
+ session.instance_variable_set '@old', @pool.get(session_id, true)
57
+ return [session_id, session]
58
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
59
+ # MemCache server cannot be contacted
60
+ warn "#{self} is unable to find memcached server."
58
61
  warn $!.inspect
59
62
  return [ nil, {} ]
60
63
  ensure
61
- @mutex.unlock if env['rack.multithread']
64
+ @mutex.unlock if @mutex.locked?
62
65
  end
63
66
 
64
67
  def set_session(env, session_id, new_session, options)
@@ -66,43 +69,50 @@ module Rack
66
69
  expiry = expiry.nil? ? 0 : expiry + 1
67
70
 
68
71
  @mutex.lock if env['rack.multithread']
69
- session = @pool.get(session_id) || {}
70
72
  if options[:renew] or options[:drop]
71
73
  @pool.delete session_id
72
74
  return false if options[:drop]
73
75
  session_id = generate_sid
74
- @pool.add session_id, 0 # so we don't worry about cache miss on #set
76
+ @pool.add session_id, {} # so we don't worry about cache miss on #set
75
77
  end
76
- old_session = new_session.instance_variable_get('@old') || {}
77
- session = merge_sessions session_id, old_session, new_session, session
78
- @pool.set session_id, session, expiry
79
- return session_id
80
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
81
- warn "#{self} is unable to find server."
82
- warn $!.inspect
83
- return false
84
- ensure
85
- @mutex.unlock if env['rack.multithread']
86
- end
87
78
 
88
- private
79
+ session = @pool.get(session_id) || {}
80
+ old_session = new_session.instance_variable_get '@old'
81
+ old_session = old_session ? Marshal.load(old_session) : {}
89
82
 
90
- def merge_sessions sid, old, new, cur=nil
91
- cur ||= {}
92
- unless Hash === old and Hash === new
93
- warn 'Bad old or new sessions provided.'
94
- return cur
95
- end
83
+ unless Hash === old_session and Hash === new_session
84
+ env['rack.errors'].
85
+ puts 'Bad old_session or new_session sessions provided.'
86
+ else # merge sessions
87
+ # alterations are either update or delete, making as few changes as
88
+ # possible to prevent possible issues.
96
89
 
97
- delete = old.keys - new.keys
98
- warn "//@#{sid}: delete #{delete*','}" if $VERBOSE and not delete.empty?
99
- delete.each{|k| cur.delete k }
90
+ # removed keys
91
+ delete = old_session.keys - new_session.keys
92
+ if $VERBOSE and not delete.empty?
93
+ env['rack.errors'].
94
+ puts "//@#{session_id}: delete #{delete*','}"
95
+ end
96
+ delete.each{|k| session.delete k }
100
97
 
101
- update = new.keys.select{|k| new[k] != old[k] }
102
- warn "//@#{sid}: update #{update*','}" if $VERBOSE and not update.empty?
103
- update.each{|k| cur[k] = new[k] }
98
+ # added or altered keys
99
+ update = new_session.keys.
100
+ select{|k| new_session[k] != old_session[k] }
101
+ if $VERBOSE and not update.empty?
102
+ env['rack.errors'].puts "//@#{session_id}: update #{update*','}"
103
+ end
104
+ update.each{|k| session[k] = new_session[k] }
105
+ end
104
106
 
105
- cur
107
+ @pool.set session_id, session, expiry
108
+ return session_id
109
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
110
+ # MemCache server cannot be contacted
111
+ warn "#{self} is unable to find memcached server."
112
+ warn $!.inspect
113
+ return false
114
+ ensure
115
+ @mutex.unlock if @mutex.locked?
106
116
  end
107
117
  end
108
118
  end
@@ -13,7 +13,7 @@ module Rack
13
13
  # In the context of a multithreaded environment, sessions being
14
14
  # committed to the pool is done in a merging manner.
15
15
  #
16
- # The :drop option is available in rack.session.options if you with to
16
+ # The :drop option is available in rack.session.options if you wish to
17
17
  # explicitly remove the session from the session cache.
18
18
  #
19
19
  # Example:
@@ -28,27 +28,28 @@ module Rack
28
28
  raise ArgumentError, "paths need to start with /"
29
29
  end
30
30
  location = location.chomp('/')
31
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
31
32
 
32
- [host, location, app]
33
- }.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
33
+ [host, location, match, app]
34
+ }.sort_by { |(h, l, m, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
34
35
  end
35
36
 
36
37
  def call(env)
37
- path = env["PATH_INFO"].to_s.squeeze("/")
38
+ path = env["PATH_INFO"].to_s
38
39
  script_name = env['SCRIPT_NAME']
39
40
  hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
40
- @mapping.each { |host, location, app|
41
+ @mapping.each { |host, location, match, app|
41
42
  next unless (hHost == host || sName == host \
42
43
  || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
43
- next unless location == path[0, location.size]
44
- next unless path[location.size] == nil || path[location.size] == ?/
44
+ next unless path =~ match && rest = $1
45
+ next unless rest.empty? || rest[0] == ?/
45
46
 
46
47
  return app.call(
47
48
  env.merge(
48
49
  'SCRIPT_NAME' => (script_name + location),
49
- 'PATH_INFO' => path[location.size..-1]))
50
+ 'PATH_INFO' => rest))
50
51
  }
51
- [404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
52
+ [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
52
53
  end
53
54
  end
54
55
  end
@@ -27,7 +27,7 @@ module Rack
27
27
  module_function :unescape
28
28
 
29
29
  DEFAULT_SEP = /[&;] */n
30
-
30
+
31
31
  # Stolen from Mongrel, with some small modifications:
32
32
  # Parses a query string by breaking it up at the '&'
33
33
  # and ';' characters. You can also use this to parse
@@ -38,7 +38,9 @@ module Rack
38
38
 
39
39
  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
40
40
  k, v = p.split('=', 2).map { |x| unescape(x) }
41
-
41
+ if v =~ /^("|')(.*)\1$/
42
+ v = $2.gsub('\\'+$1, $1)
43
+ end
42
44
  if cur = params[k]
43
45
  if cur.class == Array
44
46
  params[k] << v
@@ -67,6 +69,9 @@ module Rack
67
69
  module_function :parse_nested_query
68
70
 
69
71
  def normalize_params(params, name, v = nil)
72
+ if v and v =~ /^("|')(.*)\1$/
73
+ v = $2.gsub('\\'+$1, $1)
74
+ end
70
75
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
71
76
  k = $1 || ''
72
77
  after = $' || ''
@@ -109,6 +114,25 @@ module Rack
109
114
  end
110
115
  module_function :build_query
111
116
 
117
+ def build_nested_query(value, prefix = nil)
118
+ case value
119
+ when Array
120
+ value.map { |v|
121
+ build_nested_query(v, "#{prefix}[]")
122
+ }.join("&")
123
+ when Hash
124
+ value.map { |k, v|
125
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
126
+ }.join("&")
127
+ when String
128
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
129
+ "#{prefix}=#{escape(value)}"
130
+ else
131
+ prefix
132
+ end
133
+ end
134
+ module_function :build_nested_query
135
+
112
136
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
113
137
  def escape_html(string)
114
138
  string.to_s.gsub("&", "&amp;").
@@ -149,6 +173,54 @@ module Rack
149
173
  end
150
174
  module_function :select_best_encoding
151
175
 
176
+ def set_cookie_header!(header, key, value)
177
+ case value
178
+ when Hash
179
+ domain = "; domain=" + value[:domain] if value[:domain]
180
+ path = "; path=" + value[:path] if value[:path]
181
+ # According to RFC 2109, we need dashes here.
182
+ # N.B.: cgi.rb uses spaces...
183
+ expires = "; expires=" + value[:expires].clone.gmtime.
184
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
185
+ secure = "; secure" if value[:secure]
186
+ httponly = "; HttpOnly" if value[:httponly]
187
+ value = value[:value]
188
+ end
189
+ value = [value] unless Array === value
190
+ cookie = escape(key) + "=" +
191
+ value.map { |v| escape v }.join("&") +
192
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
193
+
194
+ case header["Set-Cookie"]
195
+ when Array
196
+ header["Set-Cookie"] << cookie
197
+ when String
198
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie]
199
+ when nil
200
+ header["Set-Cookie"] = cookie
201
+ end
202
+
203
+ nil
204
+ end
205
+ module_function :set_cookie_header!
206
+
207
+ def delete_cookie_header!(header, key, value = {})
208
+ unless Array === header["Set-Cookie"]
209
+ header["Set-Cookie"] = [header["Set-Cookie"]].compact
210
+ end
211
+
212
+ header["Set-Cookie"].reject! { |cookie|
213
+ cookie =~ /\A#{escape(key)}=/
214
+ }
215
+
216
+ set_cookie_header!(header, key,
217
+ {:value => '', :path => nil, :domain => nil,
218
+ :expires => Time.at(0) }.merge(value))
219
+
220
+ nil
221
+ end
222
+ module_function :delete_cookie_header!
223
+
152
224
  # Return the bytesize of String; uses String#length under Ruby 1.8 and
153
225
  # String#bytesize under 1.9.
154
226
  if ''.respond_to?(:bytesize)
@@ -191,11 +263,22 @@ module Rack
191
263
  # A case-insensitive Hash that preserves the original case of a
192
264
  # header when set.
193
265
  class HeaderHash < Hash
266
+ def self.new(hash={})
267
+ HeaderHash === hash ? hash : super(hash)
268
+ end
269
+
194
270
  def initialize(hash={})
271
+ super()
195
272
  @names = {}
196
273
  hash.each { |k, v| self[k] = v }
197
274
  end
198
275
 
276
+ def each
277
+ super do |k, v|
278
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
279
+ end
280
+ end
281
+
199
282
  def to_hash
200
283
  inject({}) do |hash, (k,v)|
201
284
  if v.respond_to? :to_ary
@@ -208,21 +291,24 @@ module Rack
208
291
  end
209
292
 
210
293
  def [](k)
211
- super @names[k.downcase]
294
+ super(@names[k] ||= @names[k.downcase])
212
295
  end
213
296
 
214
297
  def []=(k, v)
215
298
  delete k
216
- @names[k.downcase] = k
299
+ @names[k] = @names[k.downcase] = k
217
300
  super k, v
218
301
  end
219
302
 
220
303
  def delete(k)
221
- super @names.delete(k.downcase)
304
+ canonical = k.downcase
305
+ result = super @names.delete(canonical)
306
+ @names.delete_if { |name,| name.downcase == canonical }
307
+ result
222
308
  end
223
309
 
224
310
  def include?(k)
225
- @names.has_key? k.downcase
311
+ @names.include?(k) || @names.include?(k.downcase)
226
312
  end
227
313
 
228
314
  alias_method :has_key?, :include?
@@ -238,13 +324,23 @@ module Rack
238
324
  hash = dup
239
325
  hash.merge! other
240
326
  end
327
+
328
+ def replace(other)
329
+ clear
330
+ other.each { |k, v| self[k] = v }
331
+ self
332
+ end
241
333
  end
242
334
 
243
335
  # Every standard HTTP code mapped to the appropriate message.
244
- # Stolen from Mongrel.
336
+ # Generated with:
337
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
338
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
339
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
245
340
  HTTP_STATUS_CODES = {
246
341
  100 => 'Continue',
247
342
  101 => 'Switching Protocols',
343
+ 102 => 'Processing',
248
344
  200 => 'OK',
249
345
  201 => 'Created',
250
346
  202 => 'Accepted',
@@ -252,12 +348,15 @@ module Rack
252
348
  204 => 'No Content',
253
349
  205 => 'Reset Content',
254
350
  206 => 'Partial Content',
351
+ 207 => 'Multi-Status',
352
+ 226 => 'IM Used',
255
353
  300 => 'Multiple Choices',
256
354
  301 => 'Moved Permanently',
257
355
  302 => 'Found',
258
356
  303 => 'See Other',
259
357
  304 => 'Not Modified',
260
358
  305 => 'Use Proxy',
359
+ 306 => 'Reserved',
261
360
  307 => 'Temporary Redirect',
262
361
  400 => 'Bad Request',
263
362
  401 => 'Unauthorized',
@@ -273,27 +372,76 @@ module Rack
273
372
  411 => 'Length Required',
274
373
  412 => 'Precondition Failed',
275
374
  413 => 'Request Entity Too Large',
276
- 414 => 'Request-URI Too Large',
375
+ 414 => 'Request-URI Too Long',
277
376
  415 => 'Unsupported Media Type',
278
377
  416 => 'Requested Range Not Satisfiable',
279
378
  417 => 'Expectation Failed',
379
+ 422 => 'Unprocessable Entity',
380
+ 423 => 'Locked',
381
+ 424 => 'Failed Dependency',
382
+ 426 => 'Upgrade Required',
280
383
  500 => 'Internal Server Error',
281
384
  501 => 'Not Implemented',
282
385
  502 => 'Bad Gateway',
283
386
  503 => 'Service Unavailable',
284
387
  504 => 'Gateway Timeout',
285
- 505 => 'HTTP Version Not Supported'
388
+ 505 => 'HTTP Version Not Supported',
389
+ 506 => 'Variant Also Negotiates',
390
+ 507 => 'Insufficient Storage',
391
+ 510 => 'Not Extended',
286
392
  }
287
393
 
288
394
  # Responses with HTTP status codes that should not have an entity body
289
395
  STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
290
396
 
397
+ SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
398
+ hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
399
+ hash
400
+ }
401
+
402
+ def status_code(status)
403
+ if status.is_a?(Symbol)
404
+ SYMBOL_TO_STATUS_CODE[status] || 500
405
+ else
406
+ status.to_i
407
+ end
408
+ end
409
+ module_function :status_code
410
+
291
411
  # A multipart form data parser, adapted from IOWA.
292
412
  #
293
413
  # Usually, Rack::Request#POST takes care of calling this.
294
414
 
295
415
  module Multipart
416
+ class UploadedFile
417
+ # The filename, *not* including the path, of the "uploaded" file
418
+ attr_reader :original_filename
419
+
420
+ # The content type of the "uploaded" file
421
+ attr_accessor :content_type
422
+
423
+ def initialize(path, content_type = "text/plain", binary = false)
424
+ raise "#{path} file does not exist" unless ::File.exist?(path)
425
+ @content_type = content_type
426
+ @original_filename = ::File.basename(path)
427
+ @tempfile = Tempfile.new(@original_filename)
428
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
429
+ @tempfile.binmode if binary
430
+ FileUtils.copy_file(path, @tempfile.path)
431
+ end
432
+
433
+ def path
434
+ @tempfile.path
435
+ end
436
+ alias_method :local_path, :path
437
+
438
+ def method_missing(method_name, *args, &block) #:nodoc:
439
+ @tempfile.__send__(method_name, *args, &block)
440
+ end
441
+ end
442
+
296
443
  EOL = "\r\n"
444
+ MULTIPART_BOUNDARY = "AaB03x"
297
445
 
298
446
  def self.parse_multipart(env)
299
447
  unless env['CONTENT_TYPE'] =~
@@ -378,7 +526,7 @@ module Rack
378
526
  :name => name, :tempfile => body, :head => head}
379
527
  elsif !filename && content_type
380
528
  body.rewind
381
-
529
+
382
530
  # Generic multipart cases, not coming from a form
383
531
  data = {:type => content_type,
384
532
  :name => name, :tempfile => body, :head => head}
@@ -388,7 +536,8 @@ module Rack
388
536
 
389
537
  Utils.normalize_params(params, name, data) unless data.nil?
390
538
 
391
- break if buf.empty? || content_length == -1
539
+ # break if we're at the end of a buffer, but not if it is the end of a field
540
+ break if (buf.empty? && $1 != EOL) || content_length == -1
392
541
  }
393
542
 
394
543
  input.rewind
@@ -396,6 +545,76 @@ module Rack
396
545
  params
397
546
  end
398
547
  end
548
+
549
+ def self.build_multipart(params, first = true)
550
+ if first
551
+ unless params.is_a?(Hash)
552
+ raise ArgumentError, "value must be a Hash"
553
+ end
554
+
555
+ multipart = false
556
+ query = lambda { |value|
557
+ case value
558
+ when Array
559
+ value.each(&query)
560
+ when Hash
561
+ value.values.each(&query)
562
+ when UploadedFile
563
+ multipart = true
564
+ end
565
+ }
566
+ params.values.each(&query)
567
+ return nil unless multipart
568
+ end
569
+
570
+ flattened_params = Hash.new
571
+
572
+ params.each do |key, value|
573
+ k = first ? key.to_s : "[#{key}]"
574
+
575
+ case value
576
+ when Array
577
+ value.map { |v|
578
+ build_multipart(v, false).each { |subkey, subvalue|
579
+ flattened_params["#{k}[]#{subkey}"] = subvalue
580
+ }
581
+ }
582
+ when Hash
583
+ build_multipart(value, false).each { |subkey, subvalue|
584
+ flattened_params[k + subkey] = subvalue
585
+ }
586
+ else
587
+ flattened_params[k] = value
588
+ end
589
+ end
590
+
591
+ if first
592
+ flattened_params.map { |name, file|
593
+ if file.respond_to?(:original_filename)
594
+ ::File.open(file.path, "rb") do |f|
595
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
596
+ <<-EOF
597
+ --#{MULTIPART_BOUNDARY}\r
598
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
599
+ Content-Type: #{file.content_type}\r
600
+ Content-Length: #{::File.stat(file.path).size}\r
601
+ \r
602
+ #{f.read}\r
603
+ EOF
604
+ end
605
+ else
606
+ <<-EOF
607
+ --#{MULTIPART_BOUNDARY}\r
608
+ Content-Disposition: form-data; name="#{name}"\r
609
+ \r
610
+ #{file}\r
611
+ EOF
612
+ end
613
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
614
+ else
615
+ flattened_params
616
+ end
617
+ end
399
618
  end
400
619
  end
401
620
  end