has_global_session 0.8.2 → 0.8.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +5 -8
- data/has_global_session.gemspec +2 -2
- data/lib/has_global_session.rb +5 -1
- data/lib/has_global_session/directory.rb +14 -11
- data/lib/has_global_session/encoding.rb +37 -0
- data/lib/has_global_session/global_session.rb +133 -90
- data/lib/has_global_session/integrated_session.rb +10 -0
- data/rails/action_controller_instance_methods.rb +19 -9
- data/rails/init.rb +1 -1
- metadata +5 -4
data/README.rdoc
CHANGED
@@ -134,7 +134,7 @@ Here are some reasons you might consider dividing your systems into different
|
|
134
134
|
authorities:
|
135
135
|
* beta/staging system vs. production system
|
136
136
|
* system hosted by a third party vs. system hosted internally
|
137
|
-
* e-commerce node vs. storefront node
|
137
|
+
* e-commerce node vs. storefront node vs. admin node
|
138
138
|
|
139
139
|
== The Directory
|
140
140
|
|
@@ -142,8 +142,8 @@ The Directory is a Ruby object instantiated by HasGlobalSession in order to
|
|
142
142
|
perform lookups of public and private keys. Given an authority name (as found
|
143
143
|
in a session cookie), the Directory can find the corresponding public key.
|
144
144
|
|
145
|
-
If the local system is an authority itself,
|
146
|
-
return non-nil and #
|
145
|
+
If the local system is an authority itself, #local_authority_name will
|
146
|
+
return non-nil and #private_key will return a private key suitable for
|
147
147
|
signing session attributes.
|
148
148
|
|
149
149
|
The Directory implementation included with HasGlobalSession uses the filesystem
|
@@ -171,18 +171,15 @@ To replace or enhance the built-in Directory, simply create a new class that
|
|
171
171
|
extends Directory and put the class somewhere in your app (the lib directory
|
172
172
|
is a good choice). In the HasGlobalSession configuration file, specify the
|
173
173
|
class name of the directory under the 'common' section, like so:
|
174
|
+
|
174
175
|
common:
|
175
176
|
integrated: true
|
176
177
|
directory: MyCoolDirectory
|
177
178
|
|
178
179
|
= To-Do
|
179
180
|
|
180
|
-
* Configurable session expiry
|
181
|
-
|
182
181
|
* Option to auto-renew session
|
183
182
|
|
184
|
-
*
|
185
|
-
(e.g. cookie expires at close of browser session, not at global session
|
186
|
-
expiration!)
|
183
|
+
* Implement single sign-out via redirect
|
187
184
|
|
188
185
|
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
|
data/has_global_session.gemspec
CHANGED
@@ -7,8 +7,8 @@ spec = Gem::Specification.new do |s|
|
|
7
7
|
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
8
|
|
9
9
|
s.name = 'has_global_session'
|
10
|
-
s.version = '0.8.
|
11
|
-
s.date = '2010-06-
|
10
|
+
s.version = '0.8.3'
|
11
|
+
s.date = '2010-06-11'
|
12
12
|
|
13
13
|
s.authors = ['Tony Spataro']
|
14
14
|
s.email = 'code@tracker.xeger.net'
|
data/lib/has_global_session.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
module HasGlobalSession
|
2
2
|
class MissingConfiguration < Exception; end
|
3
|
-
class
|
3
|
+
class ConfigurationError < Exception; end
|
4
|
+
class InvalidSession < Exception; end
|
5
|
+
class UnserializableType < Exception; end
|
6
|
+
class NoAuthority < Exception; end
|
4
7
|
end
|
5
8
|
|
6
9
|
basedir = File.dirname(__FILE__)
|
7
10
|
require File.join(basedir, 'has_global_session', 'configuration')
|
8
11
|
require File.join(basedir, 'has_global_session', 'directory')
|
12
|
+
require File.join(basedir, 'has_global_session', 'encoding')
|
9
13
|
require File.join(basedir, 'has_global_session', 'global_session')
|
10
14
|
require File.join(basedir, 'has_global_session', 'integrated_session')
|
@@ -1,36 +1,39 @@
|
|
1
1
|
module HasGlobalSession
|
2
2
|
class Directory
|
3
|
-
attr_reader :authorities, :
|
3
|
+
attr_reader :authorities, :private_key, :local_authority_name
|
4
4
|
|
5
5
|
def initialize(keystore_directory)
|
6
6
|
certs = Dir[File.join(keystore_directory, '*.pub')]
|
7
7
|
keys = Dir[File.join(keystore_directory, '*.key')]
|
8
|
+
raise ConfigurationError, "Excepted 0 or 1 key files, found #{keys.size}" unless [0, 1].include?(keys.size)
|
8
9
|
|
9
10
|
@authorities = {}
|
10
11
|
certs.each do |cert_file|
|
11
12
|
basename = File.basename(cert_file)
|
12
13
|
authority = basename[0...(basename.rindex('.'))] #chop trailing .ext
|
13
14
|
@authorities[authority] = OpenSSL::PKey::RSA.new(File.read(cert_file))
|
14
|
-
raise
|
15
|
+
raise ConfigurationError, "Expected #{basename} to contain an RSA public key" unless @authorities[authority].public?
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@
|
21
|
-
raise
|
22
|
-
@
|
18
|
+
if (authority_name = Configuration['authority'])
|
19
|
+
key_file = keys.detect { |kf| kf =~ /#{authority_name}.key$/ }
|
20
|
+
raise ConfigurationError, "Key file #{authority_name}.key not found" unless key_file
|
21
|
+
@private_key = OpenSSL::PKey::RSA.new(File.read(key_file))
|
22
|
+
raise ConfigurationError, "Expected #{basename} to contain an RSA private key" unless @private_key.private?
|
23
|
+
@local_authority_name = authority_name
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
27
|
def trusted_authority?(authority)
|
27
|
-
Configuration['trust'].
|
28
|
-
authority == my_authority_name ||
|
29
|
-
Configuration['trust'].include?(authority)
|
28
|
+
Configuration['trust'].include?(authority)
|
30
29
|
end
|
31
30
|
|
32
31
|
def invalidated_session?(uuid)
|
33
32
|
false
|
34
33
|
end
|
34
|
+
|
35
|
+
def report_exception(exception, cookie=nil)
|
36
|
+
true
|
37
|
+
end
|
35
38
|
end
|
36
39
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
module Encoding
|
3
|
+
class JSON
|
4
|
+
def self.load(json)
|
5
|
+
::JSON.load(json)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.dump(object)
|
9
|
+
return object.to_json
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Implements URL encoding, but without newlines, and using '-' and '_' as
|
14
|
+
# the 62nd and 63rd symbol instead of '+' and '/'. This makes for encoded
|
15
|
+
# values that can be easily stored in a cookie; however, they cannot
|
16
|
+
# be used in a URL query string without URL-escaping them since they
|
17
|
+
# will contain '=' characters.
|
18
|
+
#
|
19
|
+
# This scheme is almost identical to the scheme "Base 64 Encoding with URL
|
20
|
+
# and Filename Safe Alphabet," described in RFC4648, with the exception that
|
21
|
+
# this scheme preserves the '=' padding characters due to limitations of
|
22
|
+
# Ruby's built-in base64 encoding routines.
|
23
|
+
class Base64
|
24
|
+
def self.load(string)
|
25
|
+
tr = string.tr('-_', '+/')
|
26
|
+
return tr.unpack('m')[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.dump(object)
|
30
|
+
raw = [object].pack('m')
|
31
|
+
raw.tr!('+/', '-_')
|
32
|
+
raw.gsub!("\n", '')
|
33
|
+
return raw
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -15,142 +15,114 @@ module HasGlobalSession
|
|
15
15
|
@directory = directory
|
16
16
|
|
17
17
|
if cookie
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
hash = JSON.load(json)
|
22
|
-
@id = hash['id']
|
23
|
-
@created_at = Time.at(hash['tc'].to_i)
|
24
|
-
@expires_at = Time.at(hash['te'].to_i)
|
25
|
-
@signed = hash['ds']
|
26
|
-
@insecure = hash['dx']
|
27
|
-
@signature = hash['s']
|
28
|
-
@authority = hash['a']
|
29
|
-
|
30
|
-
hash.delete('s')
|
31
|
-
expected = digest(hash)
|
32
|
-
signer = @directory.authorities[@authority]
|
33
|
-
raise SecurityError, "Unknown signing authority #{@authority}" unless signer
|
34
|
-
|
35
|
-
got = signer.public_decrypt(Base64.decode64(@signature))
|
36
|
-
unless (got == expected)
|
37
|
-
raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
|
38
|
-
end
|
39
|
-
|
40
|
-
unless @directory.trusted_authority?(@authority)
|
41
|
-
raise SecurityError, "Global sessions created by #{@authority} are not trusted"
|
42
|
-
end
|
43
|
-
|
44
|
-
if expired? || @directory.invalidated_session?(@id)
|
45
|
-
raise ExpiredSession, "Global session cookie has expired"
|
46
|
-
end
|
47
|
-
|
18
|
+
load_from_cookie(cookie)
|
19
|
+
elsif @directory.local_authority_name
|
20
|
+
create_from_scratch
|
48
21
|
else
|
49
|
-
|
50
|
-
@insecure = {}
|
51
|
-
|
52
|
-
if defined?(::UUIDTools) # UUIDTools v2
|
53
|
-
@id = ::UUIDTools::UUID.timestamp_create.to_s
|
54
|
-
elsif defined?(::UUID) # UUIDTools v1
|
55
|
-
@id = ::UUID.timestamp_create.to_s
|
56
|
-
else
|
57
|
-
raise TypeError, "Neither UUIDTools nor UUID defined; unsupported UUIDTools version?"
|
58
|
-
end
|
59
|
-
|
60
|
-
@created_at = Time.now.utc
|
61
|
-
@authority = @directory.my_authority_name
|
62
|
-
renew!
|
22
|
+
create_invalid
|
63
23
|
end
|
64
24
|
end
|
65
25
|
|
66
|
-
def
|
67
|
-
(@expires_at
|
68
|
-
end
|
69
|
-
|
70
|
-
def expire!
|
71
|
-
@expires_at = Time.at(0)
|
72
|
-
@dirty = true
|
73
|
-
end
|
74
|
-
|
75
|
-
def renew!
|
76
|
-
@expires_at = Configuration['timeout'].to_i.minutes.from_now.utc || 1.hours.from_now.utc
|
77
|
-
@dirty = true
|
26
|
+
def valid?
|
27
|
+
@id && (@expires_at > Time.now) && ! @directory.invalidated_session?(@id)
|
78
28
|
end
|
79
29
|
|
80
30
|
def to_s
|
31
|
+
if @cookie && !@dirty_insecure && !@dirty_secure
|
32
|
+
#use cached cookie if nothing has changed
|
33
|
+
return @cookie
|
34
|
+
end
|
35
|
+
|
81
36
|
hash = {'id'=>@id,
|
82
37
|
'tc'=>@created_at.to_i, 'te'=>@expires_at.to_i,
|
83
|
-
'ds'=>@signed
|
38
|
+
'ds'=>@signed}
|
84
39
|
|
85
|
-
if @signature && !@
|
40
|
+
if @signature && !@dirty_secure
|
86
41
|
#use cached signature unless we've changed secure state
|
87
42
|
authority = @authority
|
88
43
|
signature = @signature
|
89
44
|
else
|
90
|
-
|
45
|
+
authority_check
|
46
|
+
authority = @directory.local_authority_name
|
91
47
|
hash['a'] = authority
|
92
48
|
digest = digest(hash)
|
93
|
-
signature = Base64.
|
49
|
+
signature = Encoding::Base64.dump(@directory.private_key.private_encrypt(digest))
|
94
50
|
end
|
95
51
|
|
96
|
-
hash['
|
97
|
-
hash['
|
98
|
-
|
52
|
+
hash['dx'] = @insecure
|
53
|
+
hash['s'] = signature
|
54
|
+
hash['a'] = authority
|
55
|
+
|
56
|
+
json = Encoding::JSON.dump(hash)
|
99
57
|
zbin = Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
|
100
|
-
return Base64.
|
58
|
+
return Encoding::Base64.dump(zbin)
|
101
59
|
end
|
102
60
|
|
103
61
|
def supports_key?(key)
|
104
62
|
@schema_signed.include?(key) || @schema_insecure.include?(key)
|
105
63
|
end
|
106
64
|
|
65
|
+
def has_key?(key)
|
66
|
+
@signed.has_key(key) || @insecure.has_key?(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def keys
|
70
|
+
@signed.keys + @insecure.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
def values
|
74
|
+
@signed.values + @insecure.values
|
75
|
+
end
|
76
|
+
|
77
|
+
def each_pair(&block)
|
78
|
+
@signed.each_pair(&block)
|
79
|
+
@insecure.each_pair(&block)
|
80
|
+
end
|
81
|
+
|
107
82
|
def [](key)
|
108
83
|
@signed[key] || @insecure[key]
|
109
84
|
end
|
110
85
|
|
111
86
|
def []=(key, value)
|
112
|
-
|
113
|
-
when String, Numeric, Array
|
114
|
-
#no-op
|
115
|
-
else
|
116
|
-
raise TypeError, "Cannot store values of type #{value.class.name} reliably"
|
117
|
-
end
|
87
|
+
raise InvalidSession unless valid?
|
118
88
|
|
119
|
-
if
|
120
|
-
|
121
|
-
raise StandardError, 'Cannot change secure session attributes; we are not an authority'
|
122
|
-
end
|
89
|
+
#Ensure that the value is serializable (will raise if not)
|
90
|
+
canonicalize(value)
|
123
91
|
|
92
|
+
if @schema_signed.include?(key)
|
93
|
+
authority_check
|
124
94
|
@signed[key] = value
|
125
|
-
@
|
95
|
+
@dirty_secure = true
|
126
96
|
elsif @schema_insecure.include?(key)
|
127
97
|
@insecure[key] = value
|
98
|
+
@dirty_insecure = true
|
128
99
|
else
|
129
100
|
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
130
101
|
end
|
131
102
|
end
|
132
103
|
|
133
|
-
def
|
134
|
-
|
104
|
+
def expire!
|
105
|
+
authority_check
|
106
|
+
@expires_at = Time.at(0)
|
107
|
+
@dirty_secure = true
|
135
108
|
end
|
136
109
|
|
137
|
-
def
|
138
|
-
|
110
|
+
def renew!
|
111
|
+
authority_check
|
112
|
+
@expires_at = Configuration['timeout'].to_i.minutes.from_now.utc || 1.hours.from_now.utc
|
113
|
+
@dirty_secure = true
|
139
114
|
end
|
140
115
|
|
141
|
-
|
142
|
-
@signed.values + @insecure.values
|
143
|
-
end
|
116
|
+
private
|
144
117
|
|
145
|
-
def
|
146
|
-
@
|
147
|
-
|
118
|
+
def authority_check
|
119
|
+
unless @directory.local_authority_name
|
120
|
+
raise NoAuthority, 'Cannot change secure session attributes; we are not an authority'
|
121
|
+
end
|
148
122
|
end
|
149
123
|
|
150
|
-
private
|
151
|
-
|
152
124
|
def digest(input)
|
153
|
-
canonical = canonicalize(input)
|
125
|
+
canonical = Encoding::JSON.dump(canonicalize(input))
|
154
126
|
return Digest::SHA1.new().update(canonical).hexdigest
|
155
127
|
end
|
156
128
|
|
@@ -164,13 +136,84 @@ module HasGlobalSession
|
|
164
136
|
end
|
165
137
|
when Array
|
166
138
|
output = input.collect { |x| canonicalize(x) }
|
167
|
-
when Numeric, String
|
139
|
+
when Numeric, String, NilClass
|
168
140
|
output = input
|
169
141
|
else
|
170
|
-
raise
|
142
|
+
raise UnserializableType, "Objects of type #{input.class.name} cannot be serialized in the global session"
|
171
143
|
end
|
172
144
|
|
173
145
|
return output
|
174
146
|
end
|
147
|
+
|
148
|
+
def load_from_cookie(cookie)
|
149
|
+
zbin = Encoding::Base64.load(cookie)
|
150
|
+
json = Zlib::Inflate.inflate(zbin)
|
151
|
+
hash = Encoding::JSON.load(json)
|
152
|
+
|
153
|
+
id = hash['id']
|
154
|
+
authority = hash['a']
|
155
|
+
created_at = Time.at(hash['tc'].to_i)
|
156
|
+
expires_at = Time.at(hash['te'].to_i)
|
157
|
+
signed = hash['ds']
|
158
|
+
insecure = hash.delete('dx')
|
159
|
+
signature = hash.delete('s')
|
160
|
+
|
161
|
+
#Check signature
|
162
|
+
expected = digest(hash)
|
163
|
+
signer = @directory.authorities[authority]
|
164
|
+
raise SecurityError, "Unknown signing authority #{authority}" unless signer
|
165
|
+
got = signer.public_decrypt(Encoding::Base64.load(signature))
|
166
|
+
unless (got == expected)
|
167
|
+
raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
|
168
|
+
end
|
169
|
+
|
170
|
+
#Check trust in signing authority
|
171
|
+
unless @directory.trusted_authority?(authority)
|
172
|
+
raise SecurityError, "Global sessions created by #{authority} are not trusted"
|
173
|
+
end
|
174
|
+
|
175
|
+
#Check expiration
|
176
|
+
if expires_at <= Time.now || @directory.invalidated_session?(id)
|
177
|
+
raise ExpiredSession, "Global session cookie has expired"
|
178
|
+
end
|
179
|
+
|
180
|
+
#If all validation stuff passed, assign our instance variables.
|
181
|
+
@id = id
|
182
|
+
@authority = authority
|
183
|
+
@created_at = created_at
|
184
|
+
@expires_at = expires_at
|
185
|
+
@signed = signed
|
186
|
+
@insecure = insecure
|
187
|
+
@signature = signature
|
188
|
+
@cookie = cookie
|
189
|
+
end
|
190
|
+
|
191
|
+
def create_from_scratch
|
192
|
+
authority_check
|
193
|
+
|
194
|
+
@signed = {}
|
195
|
+
@insecure = {}
|
196
|
+
@created_at = Time.now.utc
|
197
|
+
@authority = @directory.local_authority_name
|
198
|
+
|
199
|
+
if defined?(::UUIDTools) # UUIDTools v2
|
200
|
+
@id = ::UUIDTools::UUID.timestamp_create.to_s
|
201
|
+
elsif defined?(::UUID) # UUIDTools v1
|
202
|
+
@id = ::UUID.timestamp_create.to_s
|
203
|
+
else
|
204
|
+
raise TypeError, "Neither UUIDTools nor UUID defined; unsupported UUIDTools version?"
|
205
|
+
end
|
206
|
+
|
207
|
+
renew!
|
208
|
+
end
|
209
|
+
|
210
|
+
def create_invalid
|
211
|
+
@id = nil
|
212
|
+
@created_at = Time.now
|
213
|
+
@expires_at = created_at
|
214
|
+
@signed = {}
|
215
|
+
@insecure = {}
|
216
|
+
@authority = nil
|
217
|
+
end
|
175
218
|
end
|
176
219
|
end
|
@@ -40,5 +40,15 @@ module HasGlobalSession
|
|
40
40
|
@global_session.each_pair(&block)
|
41
41
|
@local_session.each_pair(&block)
|
42
42
|
end
|
43
|
+
|
44
|
+
def method_missing(meth, *args)
|
45
|
+
if @global_session.respond_to?(meth)
|
46
|
+
return @global_session.send(meth, *args)
|
47
|
+
elsif @local_session.respond_to?(meth)
|
48
|
+
return @local_session.send(meth, *args)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
43
53
|
end
|
44
54
|
end
|
@@ -20,7 +20,8 @@ module HasGlobalSession
|
|
20
20
|
rescue Exception => e
|
21
21
|
#silently recover from any error by initializing a new global session;
|
22
22
|
#the new session will be unauthenticated.
|
23
|
-
|
23
|
+
directory.report_exception(e, cookie)
|
24
|
+
logger.error "#{e.class.name}: #{e.message} (at #{e.backtrace[0]})" if logger
|
24
25
|
@global_session = GlobalSession.new(directory)
|
25
26
|
end
|
26
27
|
end
|
@@ -34,16 +35,25 @@ module HasGlobalSession
|
|
34
35
|
|
35
36
|
def global_session_update_cookie
|
36
37
|
return unless @global_session
|
38
|
+
name = Configuration['cookie']['name']
|
39
|
+
domain = Configuration['cookie']['domain']
|
37
40
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
#Default options for invalid session
|
42
|
+
options = {:value => nil,
|
43
|
+
:domain => domain,
|
44
|
+
:expires => Time.at(0)}
|
45
|
+
|
46
|
+
if @global_session.valid?
|
47
|
+
begin
|
48
|
+
value = @global_session.to_s
|
49
|
+
expires = Configuration['ephemeral'] ? nil : @global_session.expires_at
|
50
|
+
options.merge!(:value => value, :expires => expires)
|
51
|
+
rescue Exception => e
|
52
|
+
logger.error "#{e.class.name}: #{e.message} (at #{e.backtrace[0]})" if logger
|
53
|
+
end
|
46
54
|
end
|
55
|
+
|
56
|
+
cookies[name] = options unless (cookies[name] == options[:value])
|
47
57
|
end
|
48
58
|
|
49
59
|
def log_processing
|
data/rails/init.rb
CHANGED
@@ -8,7 +8,7 @@ if File.exist?(config_file)
|
|
8
8
|
# and operating environment.
|
9
9
|
HasGlobalSession::Configuration.config_file = config_file
|
10
10
|
HasGlobalSession::Configuration.environment = RAILS_ENV
|
11
|
-
|
11
|
+
|
12
12
|
require File.join(basedir, 'rails', 'action_controller_instance_methods')
|
13
13
|
|
14
14
|
# Enable ActionController integration.
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_global_session
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 57
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 8
|
9
|
-
-
|
10
|
-
version: 0.8.
|
9
|
+
- 3
|
10
|
+
version: 0.8.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tony Spataro
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-06-
|
18
|
+
date: 2010-06-11 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- lib/has_global_session.rb
|
67
67
|
- lib/has_global_session/configuration.rb
|
68
68
|
- lib/has_global_session/directory.rb
|
69
|
+
- lib/has_global_session/encoding.rb
|
69
70
|
- lib/has_global_session/global_session.rb
|
70
71
|
- lib/has_global_session/integrated_session.rb
|
71
72
|
- rails/action_controller_instance_methods.rb
|