global_session 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +0 -10
- data/global_session.gemspec +3 -2
- data/lib/global_session/configuration.rb +5 -2
- data/lib/global_session/directory.rb +0 -1
- data/lib/global_session/encoding.rb +13 -1
- data/lib/global_session/rack.rb +1 -1
- data/lib/global_session/rails/action_controller_instance_methods.rb +0 -23
- data/lib/global_session/rails.rb +0 -2
- data/lib/global_session/session/abstract.rb +83 -0
- data/lib/global_session/session/v1.rb +352 -0
- data/lib/global_session/session/v2.rb +357 -0
- data/lib/global_session/session.rb +34 -398
- data/lib/global_session.rb +6 -7
- data/rails_generators/global_session/templates/global_session.yml.erb +8 -14
- metadata +24 -7
- data/lib/global_session/integrated_session.rb +0 -146
data/README.rdoc
CHANGED
@@ -52,15 +52,6 @@ particular, it does not provide any of the following:
|
|
52
52
|
...
|
53
53
|
@current_user = User.find(global_session['user'])
|
54
54
|
|
55
|
-
5) For easier programming, enable seamless integration with the local session:
|
56
|
-
(in global_session.yml)
|
57
|
-
common:
|
58
|
-
integrated: true
|
59
|
-
|
60
|
-
(in your controllers)
|
61
|
-
session['user'] = @user.id #goes to the global session
|
62
|
-
session['local_thingie'] = @thingie.id #goes to the local session
|
63
|
-
|
64
55
|
= Global Session Contents
|
65
56
|
|
66
57
|
Global session state is stored as a cookie in the user's browser. The cookie
|
@@ -176,7 +167,6 @@ is a good choice). In the GlobalSession configuration file, specify the
|
|
176
167
|
class name of the directory under the 'common' section, like so:
|
177
168
|
|
178
169
|
common:
|
179
|
-
integrated: true
|
180
170
|
directory: MyCoolDirectory
|
181
171
|
|
182
172
|
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
|
data/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 = 'global_session'
|
10
|
-
s.version = '
|
11
|
-
s.date = '2012-11-
|
10
|
+
s.version = '2.0.0'
|
11
|
+
s.date = '2012-11-06'
|
12
12
|
|
13
13
|
s.authors = ['Tony Spataro']
|
14
14
|
s.email = 'support@rightscale.com'
|
@@ -21,6 +21,7 @@ spec = Gem::Specification.new do |s|
|
|
21
21
|
|
22
22
|
s.add_runtime_dependency('simple_uuid', [">= 0.2.0"])
|
23
23
|
s.add_runtime_dependency('json', ["~> 1.4"])
|
24
|
+
s.add_runtime_dependency('msgpack', ["~> 0.4"])
|
24
25
|
s.add_runtime_dependency('rack-contrib', ["~> 1.0"])
|
25
26
|
|
26
27
|
basedir = File.dirname(__FILE__)
|
@@ -32,7 +32,6 @@ module GlobalSession
|
|
32
32
|
# * attributes
|
33
33
|
# * signed
|
34
34
|
# * insecure
|
35
|
-
# * integrated
|
36
35
|
# * ephemeral
|
37
36
|
# * timeout
|
38
37
|
# * renew
|
@@ -78,6 +77,10 @@ module GlobalSession
|
|
78
77
|
"<#{self.class.name} @environment=#{@environment.inspect}>"
|
79
78
|
end
|
80
79
|
|
80
|
+
def to_hash
|
81
|
+
@config.dup
|
82
|
+
end
|
83
|
+
|
81
84
|
# Create a new Configuration object
|
82
85
|
#
|
83
86
|
# === Parameters
|
@@ -118,7 +121,7 @@ module GlobalSession
|
|
118
121
|
end
|
119
122
|
|
120
123
|
def validate # :nodoc
|
121
|
-
['attributes/signed', '
|
124
|
+
['attributes/signed', 'cookie/name',
|
122
125
|
'timeout'].each {|k| validate_presence_of k}
|
123
126
|
end
|
124
127
|
|
@@ -95,7 +95,6 @@ module GlobalSession
|
|
95
95
|
# === Parameters
|
96
96
|
# directory(Directory):: directory implementation that the session should use for various operations
|
97
97
|
# cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
|
98
|
-
# valid_signature_digest(String):: Optional, already-trusted signature. If supplied, the expensive RSA-verify operation will be skipped if the cookie's signature matches the value supplied.
|
99
98
|
#
|
100
99
|
# === Return
|
101
100
|
# session(Session):: the newly-initialized session
|
@@ -26,7 +26,7 @@ module GlobalSession
|
|
26
26
|
# JSON serializer, used to serialize Hash objects in a form suitable
|
27
27
|
# for stuffing into a cookie.
|
28
28
|
#
|
29
|
-
|
29
|
+
module JSON
|
30
30
|
# Unserialize JSON to Hash.
|
31
31
|
#
|
32
32
|
# === Parameters
|
@@ -50,6 +50,18 @@ module GlobalSession
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
# Wrapper module for MessagePack that makes it conform to the standard load/dump interface
|
54
|
+
# for serializers.
|
55
|
+
module Msgpack
|
56
|
+
def self.load(binary)
|
57
|
+
MessagePack.unpack(binary)
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.dump(object)
|
61
|
+
object.to_msgpack
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
53
65
|
# Implements URL encoding, but without newlines, and using '-' and '_' as
|
54
66
|
# the 62nd and 63rd symbol instead of '+' and '/'. This makes for encoded
|
55
67
|
# values that can be easily stored in a cookie; however, they cannot
|
data/lib/global_session/rack.rb
CHANGED
@@ -69,7 +69,7 @@ module GlobalSession
|
|
69
69
|
namespace = namespace.const_get(parts.shift.to_sym) until parts.empty?
|
70
70
|
directory_klass = namespace
|
71
71
|
rescue Exception => e
|
72
|
-
raise ConfigurationError, "Invalid/unknown directory class name #{@configuration['directory']}"
|
72
|
+
raise GlobalSession::ConfigurationError, "Invalid/unknown directory class name #{@configuration['directory']}"
|
73
73
|
end
|
74
74
|
|
75
75
|
if directory.instance_of?(String)
|
@@ -37,9 +37,6 @@ module GlobalSession
|
|
37
37
|
module ActionControllerInstanceMethods
|
38
38
|
def self.included(base) # :nodoc:
|
39
39
|
#Make sure a superclass hasn't already chained the methods...
|
40
|
-
unless base.instance_methods.include?("session_without_global_session")
|
41
|
-
base.alias_method_chain :session, :global_session
|
42
|
-
end
|
43
40
|
unless base.instance_methods.include?("log_processing_without_global_session")
|
44
41
|
base.alias_method_chain :log_processing, :global_session
|
45
42
|
end
|
@@ -67,26 +64,6 @@ module GlobalSession
|
|
67
64
|
@global_session
|
68
65
|
end
|
69
66
|
|
70
|
-
# Aliased version of ActionController::Base#session which will return the integrated
|
71
|
-
# global-and-local session object (IntegratedSession).
|
72
|
-
#
|
73
|
-
# === Return
|
74
|
-
# session(IntegratedSession):: the integrated session
|
75
|
-
def session_with_global_session
|
76
|
-
if global_session_options[:integrated] && global_session
|
77
|
-
unless @integrated_session &&
|
78
|
-
(@integrated_session.local == session_without_global_session) &&
|
79
|
-
(@integrated_session.global == global_session)
|
80
|
-
@integrated_session =
|
81
|
-
IntegratedSession.new(session_without_global_session, global_session)
|
82
|
-
end
|
83
|
-
|
84
|
-
return @integrated_session
|
85
|
-
else
|
86
|
-
return session_without_global_session
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
67
|
# Filter to initialize the global session.
|
91
68
|
#
|
92
69
|
# === Return
|
data/lib/global_session/rails.rb
CHANGED
@@ -19,8 +19,6 @@
|
|
19
19
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
|
-
basedir = File.dirname(__FILE__)
|
23
|
-
|
24
22
|
require 'rack/contrib/cookies'
|
25
23
|
require 'action_pack'
|
26
24
|
require 'action_controller'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module GlobalSession::Session
|
2
|
+
# An abstract base class for all versions of the global session.
|
3
|
+
# Defines common attributes and methods.
|
4
|
+
class Abstract
|
5
|
+
attr_reader :id, :authority, :created_at, :expired_at, :directory
|
6
|
+
attr_reader :signed, :insecure
|
7
|
+
|
8
|
+
# Create a new global session object.
|
9
|
+
#
|
10
|
+
# === Parameters
|
11
|
+
# directory(Directory):: directory implementation that the session should use for various operations
|
12
|
+
#
|
13
|
+
# ===Raise
|
14
|
+
# InvalidSession:: if the session contained in the cookie has been invalidated
|
15
|
+
# ExpiredSession:: if the session contained in the cookie has expired
|
16
|
+
# MalformedCookie:: if the cookie was corrupt or malformed
|
17
|
+
# SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
|
18
|
+
def initialize(directory)
|
19
|
+
@directory = directory
|
20
|
+
@signed = {}
|
21
|
+
@insecure = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return a representation of the object suitable for printing to the console
|
25
|
+
def inspect
|
26
|
+
"<#{self.class.name}(#{self.id})>"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return a Hash representation of the session with three subkeys: :metadata, :signed and :insecure
|
30
|
+
# @raise nothing -- does not raise; returns empty hash if there is a failure
|
31
|
+
def to_hash
|
32
|
+
hash = {}
|
33
|
+
|
34
|
+
md = {}
|
35
|
+
signed = {}
|
36
|
+
insecure = {}
|
37
|
+
|
38
|
+
hash[:metadata] = md
|
39
|
+
hash[:signed] = signed
|
40
|
+
hash[:insecure] = insecure
|
41
|
+
|
42
|
+
md[:id] = @id
|
43
|
+
md[:authority] = @authority
|
44
|
+
md[:created_at] = @created_at
|
45
|
+
md[:expired_at] = @expired_at
|
46
|
+
@signed.each_pair { |k, v| signed[k] = v }
|
47
|
+
@insecure.each_pair { |k, v| insecure[k] = v }
|
48
|
+
|
49
|
+
hash
|
50
|
+
rescue Exception => e
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Invalidate this session by reporting its UUID to the Directory.
|
55
|
+
#
|
56
|
+
# === Return
|
57
|
+
# unknown(Object):: Returns whatever the Directory returns
|
58
|
+
def invalidate!
|
59
|
+
@directory.report_invalid_session(@id, @expired_at)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Renews this global session, changing its expiry timestamp into the future.
|
63
|
+
# Causes a new signature will be computed when the session is next serialized.
|
64
|
+
#
|
65
|
+
# === Return
|
66
|
+
# true:: Always returns true
|
67
|
+
def renew!(expired_at=nil)
|
68
|
+
authority_check
|
69
|
+
minutes = Integer(@configuration['timeout'])
|
70
|
+
expired_at ||= Time.at(Time.now.utc + 60 * minutes)
|
71
|
+
@expired_at = expired_at
|
72
|
+
@created_at = Time.now.utc
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def authority_check # :nodoc:
|
78
|
+
unless @directory.local_authority_name
|
79
|
+
raise GlobalSession::NoAuthority, 'Cannot change secure session attributes; we are not an authority'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,352 @@
|
|
1
|
+
# Copyright (c) 2012 RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
# Standard library dependencies
|
23
|
+
require 'set'
|
24
|
+
require 'zlib'
|
25
|
+
|
26
|
+
module GlobalSession::Session
|
27
|
+
# Ladies and gentlemen: the one and only, star of the show, GLOBAL SESSION!
|
28
|
+
#
|
29
|
+
# Session is designed to act as much like a Hash as possible. You can use
|
30
|
+
# most of the methods you would use with Hash: [], has_key?, each, etc. It has a
|
31
|
+
# few additional methods that are specific to itself, mostly involving whether
|
32
|
+
# it's expired, valid, supports a certain key, etc.
|
33
|
+
#
|
34
|
+
class V1 < Abstract
|
35
|
+
# Utility method to decode a cookie; good for console debugging. This performs no
|
36
|
+
# validation or security check of any sort.
|
37
|
+
#
|
38
|
+
# === Parameters
|
39
|
+
# cookie(String):: well-formed global session cookie
|
40
|
+
def self.decode_cookie(cookie)
|
41
|
+
zbin = GlobalSession::Encoding::Base64Cookie.load(cookie)
|
42
|
+
json = Zlib::Inflate.inflate(zbin)
|
43
|
+
return GlobalSession::Encoding::JSON.load(json)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a new global session object.
|
47
|
+
#
|
48
|
+
# === Parameters
|
49
|
+
# directory(Directory):: directory implementation that the session should use for various operations
|
50
|
+
# cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
|
51
|
+
# valid_signature_digest(String):: Optional, already-trusted signature. If supplied, the expensive RSA-verify operation will be skipped if the cookie's signature matches the value supplied.
|
52
|
+
#
|
53
|
+
# ===Raise
|
54
|
+
# InvalidSession:: if the session contained in the cookie has been invalidated
|
55
|
+
# ExpiredSession:: if the session contained in the cookie has expired
|
56
|
+
# MalformedCookie:: if the cookie was corrupt or malformed
|
57
|
+
# SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
|
58
|
+
def initialize(directory, cookie=nil, valid_signature_digest=nil)
|
59
|
+
super(directory)
|
60
|
+
@configuration = directory.configuration
|
61
|
+
@schema_signed = Set.new((@configuration['attributes']['signed']))
|
62
|
+
@schema_insecure = Set.new((@configuration['attributes']['insecure']))
|
63
|
+
|
64
|
+
if cookie && !cookie.empty?
|
65
|
+
load_from_cookie(cookie, valid_signature_digest)
|
66
|
+
elsif @directory.local_authority_name
|
67
|
+
create_from_scratch
|
68
|
+
else
|
69
|
+
create_invalid
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [true,false] true if this session was created in-process, false if it was initialized from a cookie
|
74
|
+
def new_record?
|
75
|
+
@cookie.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
# Determine whether the session is valid. This method simply delegates to the
|
79
|
+
# directory associated with this session.
|
80
|
+
#
|
81
|
+
# === Return
|
82
|
+
# valid(true|false):: True if the session is valid, false otherwise
|
83
|
+
def valid?
|
84
|
+
@directory.valid_session?(@id, @expired_at)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Serialize the session to a form suitable for use with HTTP cookies. If any
|
88
|
+
# secure attributes have changed since the session was instantiated, compute
|
89
|
+
# a fresh RSA signature.
|
90
|
+
#
|
91
|
+
# === Return
|
92
|
+
# cookie(String):: The B64cookie-encoded Zlib-compressed JSON-serialized global session hash
|
93
|
+
def to_s
|
94
|
+
if @cookie && !@dirty_insecure && !@dirty_secure
|
95
|
+
#use cached cookie if nothing has changed
|
96
|
+
return @cookie
|
97
|
+
end
|
98
|
+
|
99
|
+
hash = {'id' => @id,
|
100
|
+
'tc' => @created_at.to_i, 'te' => @expired_at.to_i,
|
101
|
+
'ds' => @signed}
|
102
|
+
|
103
|
+
if @signature && !@dirty_secure
|
104
|
+
#use cached signature unless we've changed secure state
|
105
|
+
authority = @authority
|
106
|
+
else
|
107
|
+
authority_check
|
108
|
+
authority = @directory.local_authority_name
|
109
|
+
hash['a'] = authority
|
110
|
+
digest = canonical_digest(hash)
|
111
|
+
@signature = GlobalSession::Encoding::Base64Cookie.dump(@directory.private_key.private_encrypt(digest))
|
112
|
+
end
|
113
|
+
|
114
|
+
hash['dx'] = @insecure
|
115
|
+
hash['s'] = @signature
|
116
|
+
hash['a'] = authority
|
117
|
+
|
118
|
+
json = GlobalSession::Encoding::JSON.dump(hash)
|
119
|
+
zbin = Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
|
120
|
+
return GlobalSession::Encoding::Base64Cookie.dump(zbin)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Determine whether the global session schema allows a given key to be placed
|
124
|
+
# in the global session.
|
125
|
+
#
|
126
|
+
# === Parameters
|
127
|
+
# key(String):: The name of the key
|
128
|
+
#
|
129
|
+
# === Return
|
130
|
+
# supported(true|false):: Whether the specified key is supported
|
131
|
+
def supports_key?(key)
|
132
|
+
@schema_signed.include?(key) || @schema_insecure.include?(key)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Determine whether this session contains a value with the specified key.
|
136
|
+
#
|
137
|
+
# === Parameters
|
138
|
+
# key(String):: The name of the key
|
139
|
+
#
|
140
|
+
# === Return
|
141
|
+
# contained(true|false):: Whether the session currently has a value for the specified key.
|
142
|
+
def has_key?(key)
|
143
|
+
@signed.has_key?(key) || @insecure.has_key?(key)
|
144
|
+
end
|
145
|
+
|
146
|
+
alias :key? :has_key?
|
147
|
+
|
148
|
+
# Return the keys that are currently present in the global session.
|
149
|
+
#
|
150
|
+
# === Return
|
151
|
+
# keys(Array):: List of keys contained in the global session
|
152
|
+
def keys
|
153
|
+
@signed.keys + @insecure.keys
|
154
|
+
end
|
155
|
+
|
156
|
+
# Return the values that are currently present in the global session.
|
157
|
+
#
|
158
|
+
# === Return
|
159
|
+
# values(Array):: List of values contained in the global session
|
160
|
+
def values
|
161
|
+
@signed.values + @insecure.values
|
162
|
+
end
|
163
|
+
|
164
|
+
# Iterate over each key/value pair
|
165
|
+
#
|
166
|
+
# === Block
|
167
|
+
# An iterator which will be called with each key/value pair
|
168
|
+
#
|
169
|
+
# === Return
|
170
|
+
# Returns the value of the last expression evaluated by the block
|
171
|
+
def each_pair(&block) # :yields: |key, value|
|
172
|
+
@signed.each_pair(&block)
|
173
|
+
@insecure.each_pair(&block)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Lookup a value by its key.
|
177
|
+
#
|
178
|
+
# === Parameters
|
179
|
+
# key(String):: the key
|
180
|
+
#
|
181
|
+
# === Return
|
182
|
+
# value(Object):: The value associated with +key+, or nil if +key+ is not present
|
183
|
+
def [](key)
|
184
|
+
key = key.to_s #take care of symbol-style keys
|
185
|
+
@signed[key] || @insecure[key]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Set a value in the global session hash. If the supplied key is denoted as
|
189
|
+
# secure by the global session schema, causes a new signature to be computed
|
190
|
+
# when the session is next serialized.
|
191
|
+
#
|
192
|
+
# === Parameters
|
193
|
+
# key(String):: The key to set
|
194
|
+
# value(Object):: The value to set
|
195
|
+
#
|
196
|
+
# === Return
|
197
|
+
# value(Object):: Always returns the value that was set
|
198
|
+
#
|
199
|
+
# ===Raise
|
200
|
+
# InvalidSession:: if the session has been invalidated (and therefore can't be written to)
|
201
|
+
# ArgumentError:: if the configuration doesn't define the specified key as part of the global session
|
202
|
+
# NoAuthority:: if the specified key is secure and the local node is not an authority
|
203
|
+
# UnserializableType:: if the specified value can't be serialized as JSON
|
204
|
+
def []=(key, value)
|
205
|
+
key = key.to_s #take care of symbol-style keys
|
206
|
+
raise GlobalSession::InvalidSession unless valid?
|
207
|
+
|
208
|
+
#Ensure that the value is serializable (will raise if not)
|
209
|
+
canonicalize(value)
|
210
|
+
|
211
|
+
if @schema_signed.include?(key)
|
212
|
+
authority_check
|
213
|
+
@signed[key] = value
|
214
|
+
@dirty_secure = true
|
215
|
+
elsif @schema_insecure.include?(key)
|
216
|
+
@insecure[key] = value
|
217
|
+
@dirty_insecure = true
|
218
|
+
else
|
219
|
+
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
220
|
+
end
|
221
|
+
|
222
|
+
return value
|
223
|
+
end
|
224
|
+
|
225
|
+
# Renews this global session, changing its expiry timestamp into the future.
|
226
|
+
# Causes a new signature will be computed when the session is next serialized.
|
227
|
+
#
|
228
|
+
# === Return
|
229
|
+
# true:: Always returns true
|
230
|
+
def renew!(expired_at=nil)
|
231
|
+
super(expired_at)
|
232
|
+
@dirty_secure = true
|
233
|
+
end
|
234
|
+
|
235
|
+
# Return the SHA1 hash of the most recently-computed RSA signature of this session.
|
236
|
+
# This isn't really intended for the end user; it exists so the Web framework integration
|
237
|
+
# code can optimize request speed by caching the most recently verified signature in the
|
238
|
+
# local session and avoid re-verifying it on every request.
|
239
|
+
#
|
240
|
+
# === Return
|
241
|
+
# digest(String):: SHA1 hex-digest of most-recently-computed signature
|
242
|
+
def signature_digest
|
243
|
+
@signature ? digest(@signature) : nil
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def canonical_digest(input) # :nodoc:
|
249
|
+
canonical = GlobalSession::Encoding::JSON.dump(canonicalize(input))
|
250
|
+
return digest(canonical)
|
251
|
+
end
|
252
|
+
|
253
|
+
def digest(input) # :nodoc:
|
254
|
+
return Digest::SHA1.new().update(input).hexdigest
|
255
|
+
end
|
256
|
+
|
257
|
+
def canonicalize(input) # :nodoc:
|
258
|
+
case input
|
259
|
+
when Hash
|
260
|
+
output = Array.new
|
261
|
+
ordered_keys = input.keys.sort
|
262
|
+
ordered_keys.each do |key|
|
263
|
+
output << [canonicalize(key), canonicalize(input[key])]
|
264
|
+
end
|
265
|
+
when Array
|
266
|
+
output = input.collect { |x| canonicalize(x) }
|
267
|
+
when Numeric, String, NilClass
|
268
|
+
output = input
|
269
|
+
else
|
270
|
+
raise UnserializableType, "Objects of type #{input.class.name} cannot be serialized in the global session"
|
271
|
+
end
|
272
|
+
|
273
|
+
return output
|
274
|
+
end
|
275
|
+
|
276
|
+
def load_from_cookie(cookie, valid_signature_digest) # :nodoc:
|
277
|
+
begin
|
278
|
+
zbin = GlobalSession::Encoding::Base64Cookie.load(cookie)
|
279
|
+
json = Zlib::Inflate.inflate(zbin)
|
280
|
+
hash = GlobalSession::Encoding::JSON.load(json)
|
281
|
+
rescue Exception => e
|
282
|
+
mc = GlobalSession::MalformedCookie.new("Caused by #{e.class.name}: #{e.message}")
|
283
|
+
mc.set_backtrace(e.backtrace)
|
284
|
+
raise mc
|
285
|
+
end
|
286
|
+
|
287
|
+
id = hash['id']
|
288
|
+
authority = hash['a']
|
289
|
+
created_at = Time.at(hash['tc'].to_i).utc
|
290
|
+
expired_at = Time.at(hash['te'].to_i).utc
|
291
|
+
signed = hash['ds']
|
292
|
+
insecure = hash.delete('dx')
|
293
|
+
signature = hash.delete('s')
|
294
|
+
|
295
|
+
unless valid_signature_digest == digest(signature)
|
296
|
+
#Check signature
|
297
|
+
expected = canonical_digest(hash)
|
298
|
+
signer = @directory.authorities[authority]
|
299
|
+
raise SecurityError, "Unknown signing authority #{authority}" unless signer
|
300
|
+
got = signer.public_decrypt(GlobalSession::Encoding::Base64Cookie.load(signature))
|
301
|
+
unless (got == expected)
|
302
|
+
raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
#Check trust in signing authority
|
307
|
+
unless @directory.trusted_authority?(authority)
|
308
|
+
raise SecurityError, "Global sessions signed by #{authority} are not trusted"
|
309
|
+
end
|
310
|
+
|
311
|
+
#Check expiration
|
312
|
+
unless expired_at > Time.now.utc
|
313
|
+
raise GlobalSession::ExpiredSession, "Session expired at #{expired_at}"
|
314
|
+
end
|
315
|
+
|
316
|
+
#Check other validity (delegate to directory)
|
317
|
+
unless @directory.valid_session?(id, expired_at)
|
318
|
+
raise GlobalSession::InvalidSession, "Global session has been invalidated"
|
319
|
+
end
|
320
|
+
|
321
|
+
#If all validation stuff passed, assign our instance variables.
|
322
|
+
@id = id
|
323
|
+
@authority = authority
|
324
|
+
@created_at = created_at
|
325
|
+
@expired_at = expired_at
|
326
|
+
@signed = signed
|
327
|
+
@insecure = insecure
|
328
|
+
@signature = signature
|
329
|
+
@cookie = cookie
|
330
|
+
end
|
331
|
+
|
332
|
+
def create_from_scratch # :nodoc:
|
333
|
+
authority_check
|
334
|
+
|
335
|
+
@signed = {}
|
336
|
+
@insecure = {}
|
337
|
+
@created_at = Time.now.utc
|
338
|
+
@authority = @directory.local_authority_name
|
339
|
+
@id = RightSupport::Data::UUID.generate
|
340
|
+
renew!
|
341
|
+
end
|
342
|
+
|
343
|
+
def create_invalid # :nodoc:
|
344
|
+
@id = nil
|
345
|
+
@created_at = Time.now.utc
|
346
|
+
@expired_at = created_at
|
347
|
+
@signed = {}
|
348
|
+
@insecure = {}
|
349
|
+
@authority = nil
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|