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
@@ -1,14 +1,18 @@
1
1
  require 'openssl'
2
2
  require 'rack/request'
3
3
  require 'rack/response'
4
+ require 'rack/session/abstract/id'
4
5
 
5
6
  module Rack
6
7
 
7
8
  module Session
8
9
 
9
10
  # Rack::Session::Cookie provides simple cookie based session management.
10
- # The session is a Ruby Hash stored as base64 encoded marshalled data
11
- # set to :key (default: rack.session).
11
+ # By default, the session is a Ruby Hash stored as base64 encoded marshalled
12
+ # data set to :key (default: rack.session). The object that encodes the
13
+ # session data is configurable and must respond to +encode+ and +decode+.
14
+ # Both methods must take a string and return a string.
15
+ #
12
16
  # When the secret key is set, cookie data is checked for data integrity.
13
17
  #
14
18
  # Example:
@@ -20,74 +24,123 @@ module Rack
20
24
  # :secret => 'change_me'
21
25
  #
22
26
  # All parameters are optional.
27
+ #
28
+ # Example of a cookie with no encoding:
29
+ #
30
+ # Rack::Session::Cookie.new(application, {
31
+ # :coder => Racke::Session::Cookie::Identity.new
32
+ # })
33
+ #
34
+ # Example of a cookie with custom encoding:
35
+ #
36
+ # Rack::Session::Cookie.new(application, {
37
+ # :coder => Class.new {
38
+ # def encode(str); str.reverse; end
39
+ # def decode(str); str.reverse; end
40
+ # }.new
41
+ # })
42
+ #
43
+
44
+ class Cookie < Abstract::ID
45
+ # Encode session cookies as Base64
46
+ class Base64
47
+ def encode(str)
48
+ [str].pack('m')
49
+ end
50
+
51
+ def decode(str)
52
+ str.unpack('m').first
53
+ end
23
54
 
24
- class Cookie
55
+ # Encode session cookies as Marshaled Base64 data
56
+ class Marshal < Base64
57
+ def encode(str)
58
+ super(::Marshal.dump(str))
59
+ end
25
60
 
26
- def initialize(app, options={})
27
- @app = app
28
- @key = options[:key] || "rack.session"
29
- @secret = options[:secret]
30
- warn <<-MSG unless @secret
31
- SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
32
- This poses a security threat. It is strongly recommended that you
33
- provide a secret to prevent exploits that may be possible from crafted
34
- cookies. This will not be supported in future versions of Rack, and
35
- future versions will even invalidate your existing user cookies.
36
-
37
- Called from: #{caller[0]}.
38
- MSG
39
- @default_options = {:domain => nil,
40
- :path => "/",
41
- :expire_after => nil}.merge(options)
61
+ def decode(str)
62
+ ::Marshal.load(super(str)) rescue nil
63
+ end
64
+ end
65
+ end
66
+
67
+ # Use no encoding for session cookies
68
+ class Identity
69
+ def encode(str); str; end
70
+ def decode(str); str; end
42
71
  end
43
72
 
44
- def call(env)
45
- load_session(env)
46
- status, headers, body = @app.call(env)
47
- commit_session(env, status, headers, body)
73
+ # Reverse string encoding. (trollface)
74
+ class Reverse
75
+ def encode(str); str.reverse; end
76
+ def decode(str); str.reverse; end
77
+ end
78
+
79
+ attr_reader :coder
80
+
81
+ def initialize(app, options={})
82
+ @secret = options.delete(:secret)
83
+ @coder = options.delete(:coder) || Base64::Marshal.new
84
+ super(app, options.merge!(:cookie_only => true))
48
85
  end
49
86
 
50
87
  private
51
88
 
52
89
  def load_session(env)
53
- request = Rack::Request.new(env)
54
- session_data = request.cookies[@key]
90
+ data = unpacked_cookie_data(env)
91
+ data = persistent_session_id!(data)
92
+ [data["session_id"], data]
93
+ end
55
94
 
56
- if @secret && session_data
57
- session_data, digest = session_data.split("--")
58
- session_data = nil unless Utils.secure_compare(digest, generate_hmac(session_data))
59
- end
95
+ def extract_session_id(env)
96
+ unpacked_cookie_data(env)["session_id"]
97
+ end
60
98
 
61
- begin
62
- session_data = session_data.unpack("m*").first
63
- session_data = Marshal.load(session_data)
64
- env["rack.session"] = session_data
65
- rescue
66
- env["rack.session"] = Hash.new
99
+ def unpacked_cookie_data(env)
100
+ env["rack.session.unpacked_cookie_data"] ||= begin
101
+ request = Rack::Request.new(env)
102
+ session_data = request.cookies[@key]
103
+
104
+ if @secret && session_data
105
+ session_data, digest = session_data.split("--")
106
+ session_data = nil unless digest == generate_hmac(session_data)
107
+ end
108
+
109
+ coder.decode(session_data) || {}
67
110
  end
111
+ end
68
112
 
69
- env["rack.session.options"] = @default_options.dup
113
+ def persistent_session_id!(data, sid=nil)
114
+ data ||= {}
115
+ data["session_id"] ||= sid || generate_sid
116
+ data
70
117
  end
71
118
 
72
- def commit_session(env, status, headers, body)
73
- session_data = Marshal.dump(env["rack.session"])
74
- session_data = [session_data].pack("m*")
119
+ # Overwrite set cookie to bypass content equality and always stream the cookie.
120
+
121
+ def set_cookie(env, headers, cookie)
122
+ Utils.set_cookie_header!(headers, @key, cookie)
123
+ end
124
+
125
+ def set_session(env, session_id, session, options)
126
+ session = persistent_session_id!(session, session_id)
127
+ session_data = coder.encode(session)
75
128
 
76
129
  if @secret
77
130
  session_data = "#{session_data}--#{generate_hmac(session_data)}"
78
131
  end
79
132
 
80
133
  if session_data.size > (4096 - @key.size)
81
- env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
134
+ env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
135
+ nil
82
136
  else
83
- options = env["rack.session.options"]
84
- cookie = Hash.new
85
- cookie[:value] = session_data
86
- cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
87
- Utils.set_cookie_header!(headers, @key, cookie.merge(options))
137
+ session_data
88
138
  end
139
+ end
89
140
 
90
- [status, headers, body]
141
+ def destroy_session(env, session_id, options)
142
+ # Nothing to do here, data is in the client
143
+ generate_sid unless options[:drop]
91
144
  end
92
145
 
93
146
  def generate_hmac(data)
@@ -21,6 +21,7 @@ module Rack
21
21
 
22
22
  class Memcache < Abstract::ID
23
23
  attr_reader :mutex, :pool
24
+
24
25
  DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
25
26
  :namespace => 'rack:session',
26
27
  :memcache_server => 'localhost:11211'
@@ -30,9 +31,9 @@ module Rack
30
31
 
31
32
  @mutex = Mutex.new
32
33
  mserv = @default_options[:memcache_server]
33
- mopts = @default_options.
34
- reject{|k,v| !MemCache::DEFAULT_OPTIONS.include? k }
35
- @pool = MemCache.new mserv, mopts
34
+ mopts = @default_options.reject{|k,v| !MemCache::DEFAULT_OPTIONS.include? k }
35
+
36
+ @pool = options[:cache] || MemCache.new(mserv, mopts)
36
37
  unless @pool.active? and @pool.servers.any?{|c| c.alive? }
37
38
  raise 'No memcache servers'
38
39
  end
@@ -45,75 +46,48 @@ module Rack
45
46
  end
46
47
  end
47
48
 
48
- def get_session(env, session_id)
49
- @mutex.lock if env['rack.multithread']
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}'"
49
+ def get_session(env, sid)
50
+ with_lock(env, [nil, {}]) do
51
+ unless sid and session = @pool.get(sid)
52
+ sid, session = generate_sid, {}
53
+ unless /^STORED/ =~ @pool.add(sid, session)
54
+ raise "Session collision on '#{sid.inspect}'"
55
+ end
54
56
  end
57
+ [sid, session]
55
58
  end
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."
61
- warn $!.inspect
62
- return [ nil, {} ]
63
- ensure
64
- @mutex.unlock if @mutex.locked?
65
59
  end
66
60
 
67
61
  def set_session(env, session_id, new_session, options)
68
62
  expiry = options[:expire_after]
69
63
  expiry = expiry.nil? ? 0 : expiry + 1
70
64
 
71
- @mutex.lock if env['rack.multithread']
72
- if options[:renew] or options[:drop]
73
- @pool.delete session_id
74
- return false if options[:drop]
75
- session_id = generate_sid
76
- @pool.add session_id, {} # so we don't worry about cache miss on #set
65
+ with_lock(env, false) do
66
+ @pool.set session_id, new_session, expiry
67
+ session_id
77
68
  end
69
+ end
78
70
 
79
- session = @pool.get(session_id) || {}
80
- old_session = new_session.instance_variable_get '@old'
81
- old_session = old_session ? Marshal.load(old_session) : {}
82
-
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.
89
-
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 }
97
-
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] }
71
+ def destroy_session(env, session_id, options)
72
+ with_lock(env) do
73
+ @pool.delete(session_id)
74
+ generate_sid unless options[:drop]
105
75
  end
76
+ end
106
77
 
107
- @pool.set session_id, session, expiry
108
- return session_id
78
+ def with_lock(env, default=nil)
79
+ @mutex.lock if env['rack.multithread']
80
+ yield
109
81
  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
82
+ if $VERBOSE
83
+ warn "#{self} is unable to find memcached server."
84
+ warn $!.inspect
85
+ end
86
+ default
114
87
  ensure
115
88
  @mutex.unlock if @mutex.locked?
116
89
  end
90
+
117
91
  end
118
92
  end
119
93
  end
@@ -42,59 +42,38 @@ module Rack
42
42
  end
43
43
 
44
44
  def get_session(env, sid)
45
- session = @pool[sid] if sid
46
- @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
- @pool.store sid, session
45
+ with_lock(env, [nil, {}]) do
46
+ unless sid and session = @pool[sid]
47
+ sid, session = generate_sid, {}
48
+ @pool.store sid, session
49
+ end
50
+ [sid, session]
52
51
  end
53
- session.instance_variable_set('@old', {}.merge(session))
54
- return [sid, session]
55
- ensure
56
- @mutex.unlock if env['rack.multithread']
57
52
  end
58
53
 
59
54
  def set_session(env, session_id, new_session, options)
60
- @mutex.lock if env['rack.multithread']
61
- session = @pool[session_id]
62
- if options[:renew] or options[:drop]
63
- @pool.delete session_id
64
- return false if options[:drop]
65
- session_id = generate_sid
66
- @pool.store session_id, 0
55
+ with_lock(env, false) do
56
+ @pool.store session_id, new_session
57
+ session_id
67
58
  end
68
- old_session = new_session.instance_variable_get('@old') || {}
69
- session = merge_sessions session_id, old_session, new_session, session
70
- @pool.store session_id, session
71
- return session_id
72
- rescue
73
- warn "#{new_session.inspect} has been lost."
74
- warn $!.inspect
75
- ensure
76
- @mutex.unlock if env['rack.multithread']
77
59
  end
78
60
 
79
- private
80
-
81
- def merge_sessions sid, old, new, cur=nil
82
- cur ||= {}
83
- unless Hash === old and Hash === new
84
- warn 'Bad old or new sessions provided.'
85
- return cur
61
+ def destroy_session(env, session_id, options)
62
+ with_lock(env) do
63
+ @pool.delete(session_id)
64
+ generate_sid unless options[:drop]
86
65
  end
66
+ end
87
67
 
88
- delete = old.keys - new.keys
89
- warn "//@#{sid}: dropping #{delete*','}" if $DEBUG and not delete.empty?
90
- delete.each{|k| cur.delete k }
91
-
92
- update = new.keys.select{|k| new[k] != old[k] }
93
- warn "//@#{sid}: updating #{update*','}" if $DEBUG and not update.empty?
94
- update.each{|k| cur[k] = new[k] }
95
-
96
- cur
68
+ def with_lock(env, default=nil)
69
+ @mutex.lock if env['rack.multithread']
70
+ yield
71
+ rescue
72
+ default
73
+ ensure
74
+ @mutex.unlock if @mutex.locked?
97
75
  end
76
+
98
77
  end
99
78
  end
100
79
  end
@@ -23,18 +23,45 @@ module Rack
23
23
  def call(env)
24
24
  @app.call(env)
25
25
  rescue StandardError, LoadError, SyntaxError => e
26
- backtrace = pretty(env, e)
26
+ exception_string = dump_exception(e)
27
+
28
+ env["rack.errors"].puts(exception_string)
29
+ env["rack.errors"].flush
30
+
31
+ if prefers_plain_text?(env)
32
+ content_type = "text/plain"
33
+ body = [exception_string]
34
+ else
35
+ content_type = "text/html"
36
+ body = pretty(env, e)
37
+ end
38
+
27
39
  [500,
28
- {"Content-Type" => "text/html",
29
- "Content-Length" => backtrace.join.size.to_s},
30
- backtrace]
40
+ {"Content-Type" => content_type,
41
+ "Content-Length" => Rack::Utils.bytesize(body.join).to_s},
42
+ body]
43
+ end
44
+
45
+ def prefers_plain_text?(env)
46
+ env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" && (!env["HTTP_ACCEPT"] || !env["HTTP_ACCEPT"].include?("text/html"))
47
+ end
48
+
49
+ def dump_exception(exception)
50
+ string = "#{exception.class}: #{exception.message}\n"
51
+ string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
52
+ string
31
53
  end
32
54
 
33
55
  def pretty(env, exception)
34
56
  req = Rack::Request.new(env)
35
- path = (req.script_name + req.path_info).squeeze("/")
36
57
 
37
- frames = exception.backtrace.map { |line|
58
+ # This double assignment is to prevent an "unused variable" warning on
59
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
60
+ path = path = (req.script_name + req.path_info).squeeze("/")
61
+
62
+ # This double assignment is to prevent an "unused variable" warning on
63
+ # Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
64
+ frames = frames = exception.backtrace.map { |line|
38
65
  frame = OpenStruct.new
39
66
  if line =~ /(.*?):(\d+)(:in `(.*)')?/
40
67
  frame.filename = $1
@@ -58,10 +85,6 @@ module Rack
58
85
  end
59
86
  }.compact
60
87
 
61
- env["rack.errors"].puts "#{exception.class}: #{exception.message}"
62
- env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l }
63
- env["rack.errors"].flush
64
-
65
88
  [@template.result(binding)]
66
89
  end
67
90
 
@@ -195,7 +218,13 @@ TEMPLATE = <<'HTML'
195
218
  <h2><%=h exception.message %></h2>
196
219
  <table><tr>
197
220
  <th>Ruby</th>
198
- <td><code><%=h frames.first.filename %></code>: in <code><%=h frames.first.function %></code>, line <%=h frames.first.lineno %></td>
221
+ <td>
222
+ <% if first = frames.first %>
223
+ <code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
224
+ <% else %>
225
+ unknown location
226
+ <% end %>
227
+ </td>
199
228
  </tr><tr>
200
229
  <th>Web</th>
201
230
  <td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>