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.
- data/README +9 -177
- data/Rakefile +2 -1
- data/SPEC +2 -2
- data/lib/rack.rb +2 -13
- data/lib/rack/auth/abstract/request.rb +7 -5
- data/lib/rack/auth/digest/md5.rb +6 -2
- data/lib/rack/auth/digest/params.rb +5 -7
- data/lib/rack/auth/digest/request.rb +1 -1
- data/lib/rack/backports/uri/common.rb +64 -0
- data/lib/rack/builder.rb +60 -3
- data/lib/rack/chunked.rb +29 -22
- data/lib/rack/conditionalget.rb +35 -16
- data/lib/rack/content_length.rb +3 -3
- data/lib/rack/deflater.rb +5 -2
- data/lib/rack/etag.rb +38 -10
- data/lib/rack/file.rb +76 -43
- data/lib/rack/handler.rb +13 -7
- data/lib/rack/handler/cgi.rb +0 -2
- data/lib/rack/handler/fastcgi.rb +13 -4
- data/lib/rack/handler/lsws.rb +0 -2
- data/lib/rack/handler/mongrel.rb +12 -2
- data/lib/rack/handler/scgi.rb +9 -1
- data/lib/rack/handler/thin.rb +7 -1
- data/lib/rack/handler/webrick.rb +12 -5
- data/lib/rack/lint.rb +2 -2
- data/lib/rack/lock.rb +29 -3
- data/lib/rack/methodoverride.rb +1 -1
- data/lib/rack/mime.rb +2 -2
- data/lib/rack/mock.rb +28 -33
- data/lib/rack/multipart.rb +34 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +164 -0
- data/lib/rack/multipart/uploaded_file.rb +30 -0
- data/lib/rack/request.rb +55 -19
- data/lib/rack/response.rb +10 -8
- data/lib/rack/sendfile.rb +14 -18
- data/lib/rack/server.rb +55 -8
- data/lib/rack/session/abstract/id.rb +233 -22
- data/lib/rack/session/cookie.rb +99 -46
- data/lib/rack/session/memcache.rb +30 -56
- data/lib/rack/session/pool.rb +22 -43
- data/lib/rack/showexceptions.rb +40 -11
- data/lib/rack/showstatus.rb +9 -2
- data/lib/rack/static.rb +29 -9
- data/lib/rack/urlmap.rb +6 -1
- data/lib/rack/utils.rb +67 -326
- data/rack.gemspec +2 -3
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +3 -0
- data/test/builder/options.ru +2 -0
- data/test/cgi/lighttpd.conf +1 -1
- data/test/cgi/lighttpd.errors +412 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/text +5 -0
- data/test/multipart/webkit +32 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_auth_digest.rb +20 -5
- data/test/spec_builder.rb +29 -0
- data/test/spec_cgi.rb +11 -0
- data/test/spec_chunked.rb +1 -1
- data/test/spec_commonlogger.rb +1 -1
- data/test/spec_conditionalget.rb +47 -0
- data/test/spec_content_length.rb +0 -6
- data/test/spec_content_type.rb +5 -5
- data/test/spec_deflater.rb +46 -2
- data/test/spec_etag.rb +68 -1
- data/test/spec_fastcgi.rb +11 -0
- data/test/spec_file.rb +54 -3
- data/test/spec_handler.rb +23 -5
- data/test/spec_lint.rb +2 -2
- data/test/spec_lock.rb +111 -5
- data/test/spec_methodoverride.rb +2 -2
- data/test/spec_mock.rb +3 -3
- data/test/spec_mongrel.rb +1 -2
- data/test/spec_multipart.rb +279 -0
- data/test/spec_request.rb +222 -38
- data/test/spec_response.rb +9 -3
- data/test/spec_server.rb +74 -0
- data/test/spec_session_abstract_id.rb +43 -0
- data/test/spec_session_cookie.rb +97 -15
- data/test/spec_session_memcache.rb +60 -50
- data/test/spec_session_pool.rb +63 -40
- data/test/spec_showexceptions.rb +64 -0
- data/test/spec_static.rb +23 -0
- data/test/spec_utils.rb +65 -351
- data/test/spec_webrick.rb +23 -4
- metadata +35 -15
- data/test/spec_auth.rb +0 -57
data/lib/rack/session/cookie.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
54
|
-
|
90
|
+
data = unpacked_cookie_data(env)
|
91
|
+
data = persistent_session_id!(data)
|
92
|
+
[data["session_id"], data]
|
93
|
+
end
|
55
94
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
95
|
+
def extract_session_id(env)
|
96
|
+
unpacked_cookie_data(env)["session_id"]
|
97
|
+
end
|
60
98
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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.
|
134
|
+
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
|
135
|
+
nil
|
82
136
|
else
|
83
|
-
|
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
|
-
|
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
|
-
|
35
|
-
@pool = MemCache.new
|
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,
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
108
|
-
|
78
|
+
def with_lock(env, default=nil)
|
79
|
+
@mutex.lock if env['rack.multithread']
|
80
|
+
yield
|
109
81
|
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
data/lib/rack/session/pool.rb
CHANGED
@@ -42,59 +42,38 @@ module Rack
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def get_session(env, sid)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
data/lib/rack/showexceptions.rb
CHANGED
@@ -23,18 +23,45 @@ module Rack
|
|
23
23
|
def call(env)
|
24
24
|
@app.call(env)
|
25
25
|
rescue StandardError, LoadError, SyntaxError => e
|
26
|
-
|
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" =>
|
29
|
-
"Content-Length" =>
|
30
|
-
|
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
|
-
|
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
|
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>
|