technomancy-rack 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/KNOWN-ISSUES +18 -0
  2. data/README +242 -0
  3. data/bin/rackup +183 -0
  4. data/lib/rack.rb +92 -0
  5. data/lib/rack/adapter/camping.rb +22 -0
  6. data/lib/rack/auth/abstract/handler.rb +28 -0
  7. data/lib/rack/auth/abstract/request.rb +37 -0
  8. data/lib/rack/auth/basic.rb +58 -0
  9. data/lib/rack/auth/digest/md5.rb +124 -0
  10. data/lib/rack/auth/digest/nonce.rb +51 -0
  11. data/lib/rack/auth/digest/params.rb +55 -0
  12. data/lib/rack/auth/digest/request.rb +40 -0
  13. data/lib/rack/auth/openid.rb +116 -0
  14. data/lib/rack/builder.rb +56 -0
  15. data/lib/rack/cascade.rb +36 -0
  16. data/lib/rack/commonlogger.rb +56 -0
  17. data/lib/rack/file.rb +112 -0
  18. data/lib/rack/handler/cgi.rb +57 -0
  19. data/lib/rack/handler/fastcgi.rb +83 -0
  20. data/lib/rack/handler/lsws.rb +52 -0
  21. data/lib/rack/handler/mongrel.rb +97 -0
  22. data/lib/rack/handler/scgi.rb +57 -0
  23. data/lib/rack/handler/webrick.rb +57 -0
  24. data/lib/rack/lint.rb +394 -0
  25. data/lib/rack/lobster.rb +65 -0
  26. data/lib/rack/mock.rb +172 -0
  27. data/lib/rack/recursive.rb +57 -0
  28. data/lib/rack/reloader.rb +64 -0
  29. data/lib/rack/request.rb +197 -0
  30. data/lib/rack/response.rb +166 -0
  31. data/lib/rack/session/abstract/id.rb +126 -0
  32. data/lib/rack/session/cookie.rb +71 -0
  33. data/lib/rack/session/memcache.rb +83 -0
  34. data/lib/rack/session/pool.rb +67 -0
  35. data/lib/rack/showexceptions.rb +344 -0
  36. data/lib/rack/showstatus.rb +103 -0
  37. data/lib/rack/static.rb +38 -0
  38. data/lib/rack/urlmap.rb +48 -0
  39. data/lib/rack/utils.rb +256 -0
  40. data/test/spec_rack_auth_basic.rb +69 -0
  41. data/test/spec_rack_auth_digest.rb +169 -0
  42. data/test/spec_rack_builder.rb +50 -0
  43. data/test/spec_rack_camping.rb +47 -0
  44. data/test/spec_rack_cascade.rb +50 -0
  45. data/test/spec_rack_cgi.rb +91 -0
  46. data/test/spec_rack_commonlogger.rb +32 -0
  47. data/test/spec_rack_fastcgi.rb +91 -0
  48. data/test/spec_rack_file.rb +40 -0
  49. data/test/spec_rack_lint.rb +317 -0
  50. data/test/spec_rack_lobster.rb +45 -0
  51. data/test/spec_rack_mock.rb +152 -0
  52. data/test/spec_rack_mongrel.rb +165 -0
  53. data/test/spec_rack_recursive.rb +77 -0
  54. data/test/spec_rack_request.rb +384 -0
  55. data/test/spec_rack_response.rb +167 -0
  56. data/test/spec_rack_session_cookie.rb +49 -0
  57. data/test/spec_rack_session_memcache.rb +100 -0
  58. data/test/spec_rack_session_pool.rb +84 -0
  59. data/test/spec_rack_showexceptions.rb +21 -0
  60. data/test/spec_rack_showstatus.rb +71 -0
  61. data/test/spec_rack_static.rb +37 -0
  62. data/test/spec_rack_urlmap.rb +175 -0
  63. data/test/spec_rack_utils.rb +69 -0
  64. data/test/spec_rack_webrick.rb +106 -0
  65. data/test/testrequest.rb +43 -0
  66. metadata +167 -0
@@ -0,0 +1,166 @@
1
+ require 'rack/request'
2
+ require 'rack/utils'
3
+
4
+ module Rack
5
+ # Rack::Response provides a convenient interface to create a Rack
6
+ # response.
7
+ #
8
+ # It allows setting of headers and cookies, and provides useful
9
+ # defaults (a OK response containing HTML).
10
+ #
11
+ # You can use Response#write to iteratively generate your response,
12
+ # but note that this is buffered by Rack::Response until you call
13
+ # +finish+. +finish+ however can take a block inside which calls to
14
+ # +write+ are syncronous with the Rack response.
15
+ #
16
+ # Your application's +call+ should end returning Response#finish.
17
+
18
+ class Response
19
+ def initialize(body=[], status=200, header={}, &block)
20
+ @status = status
21
+ @header = Utils::HeaderHash.new({"Content-Type" => "text/html"}.
22
+ merge(header))
23
+
24
+ @writer = lambda { |x| @body << x }
25
+ @block = nil
26
+
27
+ @body = []
28
+
29
+ if body.respond_to? :to_str
30
+ write body.to_str
31
+ elsif body.respond_to?(:each)
32
+ body.each { |part|
33
+ write part.to_s
34
+ }
35
+ else
36
+ raise TypeError, "stringable or iterable required"
37
+ end
38
+
39
+ yield self if block_given?
40
+ end
41
+
42
+ attr_reader :header
43
+ attr_accessor :status, :body
44
+
45
+ def [](key)
46
+ header[key]
47
+ end
48
+
49
+ def []=(key, value)
50
+ header[key] = value
51
+ end
52
+
53
+ def set_cookie(key, value)
54
+ case value
55
+ when Hash
56
+ domain = "; domain=" + value[:domain] if value[:domain]
57
+ path = "; path=" + value[:path] if value[:path]
58
+ # According to RFC 2109, we need dashes here.
59
+ # N.B.: cgi.rb uses spaces...
60
+ expires = "; expires=" + value[:expires].clone.gmtime.
61
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
62
+ value = value[:value]
63
+ end
64
+ value = [value] unless Array === value
65
+ cookie = Utils.escape(key) + "=" +
66
+ value.map { |v| Utils.escape v }.join("&") +
67
+ "#{domain}#{path}#{expires}"
68
+
69
+ case self["Set-Cookie"]
70
+ when Array
71
+ self["Set-Cookie"] << cookie
72
+ when String
73
+ self["Set-Cookie"] = [self["Set-Cookie"], cookie]
74
+ when nil
75
+ self["Set-Cookie"] = cookie
76
+ end
77
+ end
78
+
79
+ def delete_cookie(key, value={})
80
+ unless Array === self["Set-Cookie"]
81
+ self["Set-Cookie"] = [self["Set-Cookie"]].compact
82
+ end
83
+
84
+ self["Set-Cookie"].reject! { |cookie|
85
+ cookie =~ /\A#{Utils.escape(key)}=/
86
+ }
87
+
88
+ set_cookie(key,
89
+ {:value => '', :path => nil, :domain => nil,
90
+ :expires => Time.at(0) }.merge(value))
91
+ end
92
+
93
+
94
+ def finish(&block)
95
+ @block = block
96
+
97
+ if [204, 304].include?(status.to_i)
98
+ header.delete "Content-Type"
99
+ [status.to_i, header.to_hash, []]
100
+ else
101
+ [status.to_i, header.to_hash, self]
102
+ end
103
+ end
104
+ alias to_a finish # For *response
105
+
106
+ def each(&callback)
107
+ @body.each(&callback)
108
+ @writer = callback
109
+ @block.call(self) if @block
110
+ end
111
+
112
+ def write(str)
113
+ @writer.call str.to_s
114
+ str
115
+ end
116
+
117
+ def close
118
+ body.close if body.respond_to?(:close)
119
+ end
120
+
121
+ def empty?
122
+ @block == nil && @body.empty?
123
+ end
124
+
125
+ alias headers header
126
+
127
+ module Helpers
128
+ def invalid?; @status < 100 || @status >= 600; end
129
+
130
+ def informational?; @status >= 100 && @status < 200; end
131
+ def successful?; @status >= 200 && @status < 300; end
132
+ def redirection?; @status >= 300 && @status < 400; end
133
+ def client_error?; @status >= 400 && @status < 500; end
134
+ def server_error?; @status >= 500 && @status < 600; end
135
+
136
+ def ok?; @status == 200; end
137
+ def forbidden?; @status == 403; end
138
+ def not_found?; @status == 404; end
139
+
140
+ def redirect?; [301, 302, 303, 307].include? @status; end
141
+ def empty?; [201, 204, 304].include? @status; end
142
+
143
+ # Headers
144
+ attr_reader :headers, :original_headers
145
+
146
+ def include?(header)
147
+ !!headers[header]
148
+ end
149
+
150
+ def content_type
151
+ headers["Content-Type"]
152
+ end
153
+
154
+ def content_length
155
+ cl = headers["Content-Length"]
156
+ cl ? cl.to_i : cl
157
+ end
158
+
159
+ def location
160
+ headers["Location"]
161
+ end
162
+ end
163
+
164
+ include Helpers
165
+ end
166
+ end
@@ -0,0 +1,126 @@
1
+ # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
2
+
3
+ require 'rack/utils'
4
+
5
+ module Rack
6
+ module Session
7
+ module Abstract
8
+ # ID sets up a basic framework for implementing an id based sessioning
9
+ # service. Cookies sent to the client for maintaining sessions will only
10
+ # contain an id reference. Only #get_session and #set_session should
11
+ # need to be overwritten.
12
+ #
13
+ # All parameters are optional.
14
+ # * :key determines the name of the cookie, by default it is
15
+ # 'rack.session'
16
+ # * :domain and :path set the related cookie values, by default
17
+ # domain is nil, and the path is '/'.
18
+ # * :expire_after is the number of seconds in which the session
19
+ # cookie will expire. By default it is set not to provide any
20
+ # expiry time.
21
+ class ID
22
+ attr_reader :key
23
+ DEFAULT_OPTIONS = {
24
+ :key => 'rack.session',
25
+ :path => '/',
26
+ :domain => nil,
27
+ :expire_after => nil
28
+ }
29
+
30
+ def initialize(app, options={})
31
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
32
+ @key = @default_options[:key]
33
+ @default_context = context app
34
+ end
35
+
36
+ def call(env)
37
+ @default_context.call(env)
38
+ end
39
+
40
+ def context(app)
41
+ Rack::Utils::Context.new self, app do |env|
42
+ load_session env
43
+ response = app.call(env)
44
+ commit_session env, response
45
+ response
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Extracts the session id from provided cookies and passes it and the
52
+ # environment to #get_session. It then sets the resulting session into
53
+ # 'rack.session', and places options and session metadata into
54
+ # 'rack.session.options'.
55
+ def load_session(env)
56
+ sid = env.fetch('HTTP_COOKIE','')[/#{@key}=([^,;]+)/,1]
57
+ sid, session = get_session(env, sid)
58
+ unless session.is_a?(Hash)
59
+ puts 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
60
+ raise TypeError, 'Session not a Hash'
61
+ end
62
+ options = @default_options.
63
+ merge({ :id => sid, :by => self, :at => Time.now })
64
+
65
+ env['rack.session'] = session
66
+ env['rack.session.options'] = options
67
+ end
68
+
69
+ # Acquires the session from the environment and the session id from
70
+ # the session options and passes them to #set_session. It then
71
+ # proceeds to set a cookie up in the response with the session's id.
72
+ def commit_session(env, response)
73
+ unless response.is_a?(Array)
74
+ puts 'Response: '+response.inspect if $DEBUG
75
+ raise ArgumentError, 'Response is not an array.'
76
+ end
77
+
78
+ options = env['rack.session.options']
79
+ unless options.is_a?(Hash)
80
+ puts 'Options: '+options.inspect if $DEBUG
81
+ raise TypeError, 'Options not a Hash'
82
+ end
83
+
84
+ sid, time, z = options.values_at(:id, :at, :by)
85
+ unless self == z
86
+ warn "#{self} not managing this session."
87
+ return
88
+ end
89
+
90
+ unless env['rack.session'].is_a?(Hash)
91
+ puts 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG
92
+ raise TypeError, 'Session not a Hash'
93
+ end
94
+ set_session(env, sid)
95
+
96
+ expiry = options[:expire_after] && time+options[:expire_after]
97
+ cookie = Utils.escape(@key)+'='+Utils.escape(sid)
98
+ cookie<< "; domain=#{options[:domain]}" if options[:domain]
99
+ cookie<< "; path=#{options[:path]}" if options[:path]
100
+ cookie<< "; expires=#{expiry}" if expiry
101
+
102
+ puts "Cookie: "+cookie.inspect if $DEBUG
103
+
104
+ case a = (h = response[1])['Set-Cookie']
105
+ when Array then a << cookie
106
+ when String then h['Set-Cookie'] = [a, cookie]
107
+ when nil then h['Set-Cookie'] = cookie
108
+ end
109
+ end
110
+
111
+ # Should return [session_id, session]. All thread safety and session
112
+ # retrival proceedures should occur here.
113
+ # If nil is provided as the session id, generation of a new valid id
114
+ # should occur within.
115
+ def get_session(env, sid)
116
+ raise '#get_session needs to be implemented.'
117
+ end
118
+
119
+ # All thread safety and session storage proceedures should occur here.
120
+ def set_session(env, sid)
121
+ raise '#set_session needs to be implemented.'
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,71 @@
1
+ module Rack
2
+
3
+ module Session
4
+
5
+ # Rack::Session::Cookie provides simple cookie based session management.
6
+ # The session is a Ruby Hash stored as base64 encoded marshalled data
7
+ # set to :key (default: rack.session).
8
+ #
9
+ # Example:
10
+ #
11
+ # use Rack::Session::Cookie, :key => 'rack.session',
12
+ # :domain => 'foo.com',
13
+ # :path => '/',
14
+ # :expire_after => 2592000
15
+ #
16
+ # All parameters are optional.
17
+
18
+ class Cookie
19
+
20
+ def initialize(app, options={})
21
+ @app = app
22
+ @key = options[:key] || "rack.session"
23
+ @default_options = {:domain => nil,
24
+ :path => "/",
25
+ :expire_after => nil}.merge(options)
26
+ end
27
+
28
+ def call(env)
29
+ load_session(env)
30
+ status, headers, body = @app.call(env)
31
+ commit_session(env, status, headers, body)
32
+ end
33
+
34
+ private
35
+
36
+ def load_session(env)
37
+ request = Rack::Request.new(env)
38
+ session_data = request.cookies[@key]
39
+
40
+ begin
41
+ session_data = session_data.unpack("m*").first
42
+ session_data = Marshal.load(session_data)
43
+ env["rack.session"] = session_data
44
+ rescue
45
+ env["rack.session"] = Hash.new
46
+ end
47
+
48
+ env["rack.session.options"] = @default_options.dup
49
+ end
50
+
51
+ def commit_session(env, status, headers, body)
52
+ session_data = Marshal.dump(env["rack.session"])
53
+ session_data = [session_data].pack("m*")
54
+
55
+ if session_data.size > (4096 - @key.size)
56
+ env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
57
+ [status, headers, body]
58
+ else
59
+ options = env["rack.session.options"]
60
+ cookie = Hash.new
61
+ cookie[:value] = session_data
62
+ cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
63
+ response = Rack::Response.new(body, status, headers)
64
+ response.set_cookie(@key, cookie.merge(options))
65
+ response.to_a
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,83 @@
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 differences.
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.
17
+ #
18
+ # Note that memcache does drop data before it may be listed to expire. For
19
+ # a full description of behaviour, please see memcache's documentation.
20
+
21
+ class Memcache < Abstract::ID
22
+ attr_reader :mutex, :pool
23
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge({
24
+ :namespace => 'rack:session',
25
+ :memcache_server => 'localhost:11211'
26
+ })
27
+
28
+ def initialize(app, options={})
29
+ super
30
+ @pool = MemCache.new @default_options[:memcache_server], @default_options
31
+ @mutex = Mutex.new
32
+ end
33
+
34
+ private
35
+
36
+ def get_session(env, sid)
37
+ session = sid && @pool.get(sid)
38
+ unless session and session.is_a?(Hash)
39
+ session = {}
40
+ lc = 0
41
+ @mutex.synchronize do
42
+ begin
43
+ raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000
44
+ sid = "%08x" % rand(0xffffffff)
45
+ ret = @pool.add(sid, session)
46
+ end until /^STORED/ =~ ret
47
+ end
48
+ end
49
+ class << session
50
+ @deleted = []
51
+ def delete key
52
+ (@deleted||=[]) << key
53
+ super
54
+ end
55
+ end
56
+ [sid, session]
57
+ end
58
+
59
+ def set_session(env, sid)
60
+ session = env['rack.session']
61
+ options = env['rack.session.options']
62
+ expiry = options[:expire_after] || 0
63
+ o, s = @mutex.synchronize do
64
+ old_session = @pool.get(sid)
65
+ unless old_session.is_a?(Hash)
66
+ warn 'Session not properly initialized.' if $DEBUG
67
+ old_session = {}
68
+ @pool.add sid, old_session, expiry
69
+ end
70
+ session.instance_eval do
71
+ @deleted.each{|k| old_session.delete(k) } if defined? @deleted
72
+ end
73
+ @pool.set sid, old_session.merge(session), expiry
74
+ [old_session, session]
75
+ end
76
+ s.each do |k,v|
77
+ next unless o.has_key?(k) and v != o[k]
78
+ warn "session value assignment collision at #{k}: #{o[k]} <- #{v}"
79
+ end if $DEBUG and env['rack.multithread']
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
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'
8
+
9
+ module Rack
10
+ module Session
11
+ # Rack::Session::Pool provides simple cookie based session management.
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.
15
+ #
16
+ # Example:
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
25
+
26
+ class Pool < Abstract::ID
27
+ attr_reader :mutex, :pool
28
+ DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.dup
29
+
30
+ def initialize(app, options={})
31
+ super
32
+ @pool = Hash.new
33
+ @mutex = Mutex.new
34
+ end
35
+
36
+ private
37
+
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]
49
+ end
50
+
51
+ def set_session(env, sid)
52
+ options = env['rack.session.options']
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']
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end