rack 0.3.0 → 0.4.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 (50) hide show
  1. data/AUTHORS +1 -0
  2. data/RDOX +61 -3
  3. data/README +94 -9
  4. data/Rakefile +36 -32
  5. data/SPEC +1 -7
  6. data/bin/rackup +31 -13
  7. data/lib/rack.rb +8 -19
  8. data/lib/rack/auth/digest/params.rb +2 -2
  9. data/lib/rack/auth/openid.rb +406 -80
  10. data/lib/rack/builder.rb +1 -1
  11. data/lib/rack/cascade.rb +10 -0
  12. data/lib/rack/commonlogger.rb +6 -1
  13. data/lib/rack/deflater.rb +63 -0
  14. data/lib/rack/directory.rb +158 -0
  15. data/lib/rack/file.rb +11 -5
  16. data/lib/rack/handler.rb +44 -0
  17. data/lib/rack/handler/evented_mongrel.rb +8 -0
  18. data/lib/rack/handler/fastcgi.rb +1 -0
  19. data/lib/rack/handler/mongrel.rb +21 -1
  20. data/lib/rack/lint.rb +20 -13
  21. data/lib/rack/mock.rb +1 -0
  22. data/lib/rack/request.rb +69 -2
  23. data/lib/rack/session/abstract/id.rb +140 -0
  24. data/lib/rack/session/memcache.rb +97 -0
  25. data/lib/rack/session/pool.rb +50 -59
  26. data/lib/rack/showstatus.rb +3 -1
  27. data/lib/rack/urlmap.rb +12 -12
  28. data/lib/rack/utils.rb +88 -9
  29. data/test/cgi/lighttpd.conf +1 -1
  30. data/test/cgi/test.fcgi +1 -2
  31. data/test/cgi/test.ru +2 -2
  32. data/test/spec_rack_auth_openid.rb +137 -0
  33. data/test/spec_rack_camping.rb +37 -33
  34. data/test/spec_rack_cascade.rb +15 -0
  35. data/test/spec_rack_cgi.rb +4 -3
  36. data/test/spec_rack_deflater.rb +70 -0
  37. data/test/spec_rack_directory.rb +56 -0
  38. data/test/spec_rack_fastcgi.rb +4 -3
  39. data/test/spec_rack_file.rb +11 -1
  40. data/test/spec_rack_handler.rb +24 -0
  41. data/test/spec_rack_lint.rb +19 -33
  42. data/test/spec_rack_mongrel.rb +71 -0
  43. data/test/spec_rack_request.rb +91 -1
  44. data/test/spec_rack_session_memcache.rb +132 -0
  45. data/test/spec_rack_session_pool.rb +48 -1
  46. data/test/spec_rack_showstatus.rb +5 -4
  47. data/test/spec_rack_urlmap.rb +60 -25
  48. data/test/spec_rack_utils.rb +118 -1
  49. data/test/testrequest.rb +3 -1
  50. metadata +67 -44
@@ -119,6 +119,7 @@ module Rack
119
119
  values.each { |value|
120
120
  @headers[field] = value
121
121
  }
122
+ @headers[field] = "" if values.empty?
122
123
  }
123
124
 
124
125
  @body = ""
@@ -24,6 +24,38 @@ module Rack
24
24
  def port; @env["SERVER_PORT"].to_i end
25
25
  def request_method; @env["REQUEST_METHOD"] end
26
26
  def query_string; @env["QUERY_STRING"].to_s end
27
+ def content_length; @env['CONTENT_LENGTH'] end
28
+ def content_type; @env['CONTENT_TYPE'] end
29
+
30
+ # The media type (type/subtype) portion of the CONTENT_TYPE header
31
+ # without any media type parameters. e.g., when CONTENT_TYPE is
32
+ # "text/plain;charset=utf-8", the media-type is "text/plain".
33
+ #
34
+ # For more information on the use of media types in HTTP, see:
35
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
36
+ def media_type
37
+ content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase
38
+ end
39
+
40
+ # The media type parameters provided in CONTENT_TYPE as a Hash, or
41
+ # an empty Hash if no CONTENT_TYPE or media-type parameters were
42
+ # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
43
+ # this method responds with the following Hash:
44
+ # { 'charset' => 'utf-8' }
45
+ def media_type_params
46
+ return {} if content_type.nil?
47
+ content_type.split(/\s*[;,]\s*/)[1..-1].
48
+ collect { |s| s.split('=', 2) }.
49
+ inject({}) { |hash,(k,v)| hash[k.downcase] = v ; hash }
50
+ end
51
+
52
+ # The character set of the request body if a "charset" media type
53
+ # parameter was given, or nil if no "charset" was specified. Note
54
+ # that, per RFC2616, text/* media types that specify no explicit
55
+ # charset are to be considered ISO-8859-1.
56
+ def content_charset
57
+ media_type_params['charset']
58
+ end
27
59
 
28
60
  def host
29
61
  # Remove port number.
@@ -37,6 +69,25 @@ module Rack
37
69
  def post?; request_method == "POST" end
38
70
  def put?; request_method == "PUT" end
39
71
  def delete?; request_method == "DELETE" end
72
+ def head?; request_method == "HEAD" end
73
+
74
+ # The set of form-data media-types. Requests that do not indicate
75
+ # one of the media types presents in this list will not be eligible
76
+ # for form-data / param parsing.
77
+ FORM_DATA_MEDIA_TYPES = [
78
+ nil,
79
+ 'application/x-www-form-urlencoded',
80
+ 'multipart/form-data'
81
+ ]
82
+
83
+ # Determine whether the request body contains form-data by checking
84
+ # the request media_type against registered form-data media-types:
85
+ # "application/x-www-form-urlencoded" and "multipart/form-data". The
86
+ # list of form-data media types can be modified through the
87
+ # +FORM_DATA_MEDIA_TYPES+ array.
88
+ def form_data?
89
+ FORM_DATA_MEDIA_TYPES.include?(media_type)
90
+ end
40
91
 
41
92
  # Returns the data recieved in the query string.
42
93
  def GET
@@ -54,9 +105,9 @@ module Rack
54
105
  # This method support both application/x-www-form-urlencoded and
55
106
  # multipart/form-data.
56
107
  def POST
57
- if @env["rack.request.form_input"] == @env["rack.input"]
108
+ if @env["rack.request.form_input"].eql? @env["rack.input"]
58
109
  @env["rack.request.form_hash"]
59
- else
110
+ elsif form_data?
60
111
  @env["rack.request.form_input"] = @env["rack.input"]
61
112
  unless @env["rack.request.form_hash"] =
62
113
  Utils::Multipart.parse_multipart(env)
@@ -64,12 +115,16 @@ module Rack
64
115
  @env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"])
65
116
  end
66
117
  @env["rack.request.form_hash"]
118
+ else
119
+ {}
67
120
  end
68
121
  end
69
122
 
70
123
  # The union of GET and POST data.
71
124
  def params
72
125
  self.GET.update(self.POST)
126
+ rescue EOFError => e
127
+ self.GET
73
128
  end
74
129
 
75
130
  # shortcut for request.params[key]
@@ -138,5 +193,17 @@ module Rack
138
193
  path << "?" << query_string unless query_string.empty?
139
194
  path
140
195
  end
196
+
197
+ def accept_encoding
198
+ @env["HTTP_ACCEPT_ENCODING"].to_s.split(/,\s*/).map do |part|
199
+ m = /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) # From WEBrick
200
+
201
+ if m
202
+ [m[1], (m[2] || 1.0).to_f]
203
+ else
204
+ raise "Invalid value for Accept-Encoding: #{part.inspect}"
205
+ end
206
+ end
207
+ end
141
208
  end
142
209
  end
@@ -0,0 +1,140 @@
1
+ # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
+ # bugrep: Andreas Zehnder
3
+
4
+ require 'rack/utils'
5
+ require 'time'
6
+
7
+ module Rack
8
+ module Session
9
+ module Abstract
10
+ # ID sets up a basic framework for implementing an id based sessioning
11
+ # service. Cookies sent to the client for maintaining sessions will only
12
+ # contain an id reference. Only #get_session and #set_session should
13
+ # need to be overwritten.
14
+ #
15
+ # All parameters are optional.
16
+ # * :key determines the name of the cookie, by default it is
17
+ # 'rack.session'
18
+ # * :domain and :path set the related cookie values, by default
19
+ # domain is nil, and the path is '/'.
20
+ # * :expire_after is the number of seconds in which the session
21
+ # cookie will expire. By default it is set not to provide any
22
+ # expiry time.
23
+ class ID
24
+ attr_reader :key
25
+ DEFAULT_OPTIONS = {
26
+ :key => 'rack.session',
27
+ :path => '/',
28
+ :domain => nil,
29
+ :expire_after => nil
30
+ }
31
+
32
+ def initialize(app, options={})
33
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
34
+ @key = @default_options[:key]
35
+ @default_context = context app
36
+ end
37
+
38
+ def call(env)
39
+ @default_context.call(env)
40
+ end
41
+
42
+ def context(app)
43
+ Rack::Utils::Context.new self, app do |env|
44
+ load_session env
45
+ response = app.call(env)
46
+ commit_session env, response
47
+ response
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Extracts the session id from provided cookies and passes it and the
54
+ # environment to #get_session. It then sets the resulting session into
55
+ # 'rack.session', and places options and session metadata into
56
+ # 'rack.session.options'.
57
+ def load_session(env)
58
+ sid = (env['HTTP_COOKIE']||'')[/#{@key}=([^,;]+)/,1]
59
+ sid, session = get_session(env, sid)
60
+ unless session.is_a?(Hash)
61
+ puts 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
62
+ raise TypeError, 'Session not a Hash'
63
+ end
64
+
65
+ options = @default_options.
66
+ merge({ :id => sid, :by => self, :at => Time.now })
67
+
68
+ env['rack.session'] = session
69
+ env['rack.session.options'] = options
70
+
71
+ return true
72
+ end
73
+
74
+ # Acquires the session from the environment and the session id from
75
+ # the session options and passes them to #set_session. It then
76
+ # proceeds to set a cookie up in the response with the session's id.
77
+ def commit_session(env, response)
78
+ unless response.is_a?(Array)
79
+ puts 'Response: '+response.inspect if $DEBUG
80
+ raise ArgumentError, 'Response is not an array.'
81
+ end
82
+
83
+ options = env['rack.session.options']
84
+ unless options.is_a?(Hash)
85
+ puts 'Options: '+options.inspect if $DEBUG
86
+ raise TypeError, 'Options not a Hash'
87
+ end
88
+
89
+ sid, time, z = options.values_at(:id, :at, :by)
90
+ unless self == z
91
+ warn "#{self} not managing this session."
92
+ return
93
+ end
94
+
95
+ unless env['rack.session'].is_a?(Hash)
96
+ warn 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
97
+ raise TypeError, 'Session not a Hash'
98
+ end
99
+
100
+ unless set_session(env, sid)
101
+ warn "Session not saved." if $DEBUG
102
+ warn "#{env['rack.session'].inspect} has been lost."if $DEBUG
103
+ return false
104
+ end
105
+
106
+ cookie = Utils.escape(@key)+'='+Utils.escape(sid)
107
+ cookie<< "; domain=#{options[:domain]}" if options[:domain]
108
+ cookie<< "; path=#{options[:path]}" if options[:path]
109
+ if options[:expire_after]
110
+ expiry = time + options[:expire_after]
111
+ cookie<< "; expires=#{expiry.httpdate}"
112
+ end
113
+
114
+ case a = (h = response[1])['Set-Cookie']
115
+ when Array then a << cookie
116
+ when String then h['Set-Cookie'] = [a, cookie]
117
+ when nil then h['Set-Cookie'] = cookie
118
+ end
119
+
120
+ return true
121
+ end
122
+
123
+ # Should return [session_id, session]. All thread safety and session
124
+ # retrival proceedures should occur here.
125
+ # If nil is provided as the session id, generation of a new valid id
126
+ # should occur within.
127
+ def get_session(env, sid)
128
+ raise '#get_session needs to be implemented.'
129
+ end
130
+
131
+ # All thread safety and session storage proceedures should occur here.
132
+ # Should return true or false dependant on whether or not the session
133
+ # was saved or not.
134
+ def set_session(env, sid)
135
+ raise '#set_session needs to be implemented.'
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,97 @@
1
+ # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
+
3
+ require 'rack/session/abstract/id'
4
+ require 'memcache'
5
+
6
+ module Rack
7
+ module Session
8
+ # Rack::Session::Memcache provides simple cookie based session management.
9
+ # Session data is stored in memcached. The corresponding session key is
10
+ # maintained in the cookie.
11
+ # You may treat Session::Memcache as you would Session::Pool with the
12
+ # following caveats.
13
+ #
14
+ # * Setting :expire_after to 0 would note to the Memcache server to hang
15
+ # onto the session data until it would drop it according to it's own
16
+ # specifications. However, the cookie sent to the client would expire
17
+ # immediately.
18
+ #
19
+ # Note that memcache does drop data before it may be listed to expire. For
20
+ # a full description of behaviour, please see memcache's documentation.
21
+
22
+ class Memcache < Abstract::ID
23
+ attr_reader :mutex, :pool
24
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge({
25
+ :namespace => 'rack:session',
26
+ :memcache_server => 'localhost:11211'
27
+ })
28
+
29
+ def initialize(app, options={})
30
+ super
31
+ @pool = MemCache.new @default_options[:memcache_server], @default_options
32
+ unless @pool.servers.any?{|s|s.alive?}
33
+ raise "#{self} unable to find server during initialization."
34
+ end
35
+ @mutex = Mutex.new
36
+ end
37
+
38
+ private
39
+
40
+ def get_session(env, sid)
41
+ session = sid && @pool.get(sid)
42
+ unless session and session.is_a?(Hash)
43
+ session = {}
44
+ lc = 0
45
+ @mutex.synchronize do
46
+ begin
47
+ raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000
48
+ sid = "%08x" % rand(0xffffffff)
49
+ ret = @pool.add(sid, session)
50
+ end until /^STORED/ =~ ret
51
+ end
52
+ end
53
+ class << session
54
+ @deleted = []
55
+ def delete key
56
+ (@deleted||=[]) << key
57
+ super
58
+ end
59
+ end
60
+ [sid, session]
61
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
62
+ warn "#{self} is unable to find server."
63
+ warn $!.inspect
64
+ return [ nil, {} ]
65
+ end
66
+
67
+ def set_session(env, sid)
68
+ session = env['rack.session']
69
+ options = env['rack.session.options']
70
+ expiry = options[:expire_after] || 0
71
+ o, s = @mutex.synchronize do
72
+ old_session = @pool.get(sid)
73
+ unless old_session.is_a?(Hash)
74
+ warn 'Session not properly initialized.' if $DEBUG
75
+ old_session = {}
76
+ @pool.add sid, old_session, expiry
77
+ end
78
+ session.instance_eval do
79
+ @deleted.each{|k| old_session.delete(k) } if defined? @deleted
80
+ end
81
+ @pool.set sid, old_session.merge(session), expiry
82
+ [old_session, session]
83
+ end
84
+ s.each do |k,v|
85
+ next unless o.has_key?(k) and v != o[k]
86
+ warn "session value assignment collision at #{k.inspect}:"+
87
+ "\n\t#{o[k].inspect}\n\t#{v.inspect}"
88
+ end if $DEBUG and env['rack.multithread']
89
+ return true
90
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted
91
+ warn "#{self} is unable to find server."
92
+ warn $!.inspect
93
+ return false
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,82 +1,73 @@
1
1
  # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
+ # THANKS:
3
+ # apeiros, for session id generation, expiry setup, and threadiness
4
+ # sergio, threadiness and bugreps
5
+
6
+ require 'rack/session/abstract/id'
7
+ require 'thread'
2
8
 
3
9
  module Rack
4
10
  module Session
5
11
  # Rack::Session::Pool provides simple cookie based session management.
6
- # Session data is stored in a hash held by @pool. The corresponding
7
- # session key sent to the client.
8
- # The pool is unmonitored and unregulated, which means that over
9
- # prolonged use the session pool will be very large.
12
+ # Session data is stored in a hash held by @pool.
13
+ # In the context of a multithreaded environment, sessions being
14
+ # committed to the pool is done in a merging manner.
10
15
  #
11
16
  # Example:
12
- #
13
- # use Rack::Session::Pool, :key => 'rack.session',
14
- # :domain => 'foo.com',
15
- # :path => '/',
16
- # :expire_after => 2592000
17
- #
18
- # All parameters are optional.
17
+ # myapp = MyRackApp.new
18
+ # sessioned = Rack::Session::Pool.new(myapp,
19
+ # :key => 'rack.session',
20
+ # :domain => 'foo.com',
21
+ # :path => '/',
22
+ # :expire_after => 2592000
23
+ # )
24
+ # Rack::Handler::WEBrick.run sessioned
19
25
 
20
- class Pool
21
- attr_reader :pool, :key
26
+ class Pool < Abstract::ID
27
+ attr_reader :mutex, :pool
28
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.dup
22
29
 
23
30
  def initialize(app, options={})
24
- @app = app
25
- @key = options[:key] || "rack.session"
26
- @default_options = {:domain => nil,
27
- :path => "/",
28
- :expire_after => nil}.merge(options)
31
+ super
29
32
  @pool = Hash.new
30
- @default_context = context app, &nil
31
- end
32
-
33
-
34
- def call(env)
35
- @default_context.call(env)
36
- end
37
-
38
- def context(app, &block)
39
- Rack::Utils::Context.new self, app do |env|
40
- load_session env
41
- block[env] if block
42
- response = app.call(env)
43
- commit_session env, response
44
- response
45
- end
33
+ @mutex = Mutex.new
46
34
  end
47
35
 
48
36
  private
49
37
 
50
- def load_session(env)
51
- sess_id = env.fetch('HTTP_COOKIE','')[/#{@key}=([^,;]+)/,1]
52
- begin
53
- sess_id = Array.new(8){rand(16).to_s(16)}*''
54
- end while @pool.key? sess_id if sess_id.nil? or !@pool.key? sess_id
55
-
56
- session = @pool.fetch sess_id, {}
57
- session.instance_variable_set '@dat', [sess_id, Time.now]
58
-
59
- @pool.store sess_id, env['rack.session'] = session
60
- env["rack.session.options"] = @default_options.dup
38
+ def get_session(env, sid)
39
+ session = @mutex.synchronize do
40
+ unless sess = @pool[sid] and ((expires = sess[:expire_at]).nil? or expires > Time.now)
41
+ @pool.delete_if{|k,v| expiry = v[:expire_at] and expiry < Time.now }
42
+ begin
43
+ sid = "%08x" % rand(0xffffffff)
44
+ end while @pool.has_key?(sid)
45
+ end
46
+ @pool[sid] ||= {}
47
+ end
48
+ [sid, session]
61
49
  end
62
50
 
63
- def commit_session(env, response)
64
- session = env['rack.session']
51
+ def set_session(env, sid)
65
52
  options = env['rack.session.options']
66
- sdat = session.instance_variable_get '@dat'
67
-
68
- cookie = Utils.escape(@key)+'='+Utils.escape(sdat[0])
69
- cookie<< "; domain=#{options[:domain]}" if options[:domain]
70
- cookie<< "; path=#{options[:path]}" if options[:path]
71
- cookie<< "; expires=#{sdat[1]+options[:expires_after]}" if options[:expires_after]
72
-
73
- case a = (h = response[1])['Set-Cookie']
74
- when Array then a << cookie
75
- when String then h['Set-Cookie'] = [a, cookie]
76
- when nil then h['Set-Cookie'] = cookie
53
+ expiry = options[:expire_after] && options[:at]+options[:expire_after]
54
+ @mutex.synchronize do
55
+ old_session = @pool[sid]
56
+ old_session[:expire_at] = expiry if expiry
57
+ session = old_session.merge(env['rack.session'])
58
+ @pool[sid] = session
59
+ session.each do |k,v|
60
+ next unless old_session.has_key?(k) and v != old_session[k]
61
+ warn "session value assignment collision at #{k}: #{old_session[k]} <- #{v}"
62
+ end if $DEBUG and env['rack.multithread']
77
63
  end
64
+ return true
65
+ rescue
66
+ warn "#{self} is unable to find server."
67
+ warn "#{env['rack.session'].inspect} has been lost."
68
+ warn $!.inspect
69
+ return false
78
70
  end
79
-
80
71
  end
81
72
  end
82
73
  end