Class: GlobalSession::Rack::Middleware

Inherits:
Object
  • Object
show all
Defined in:
lib/global_session/rack.rb

Overview

Global session middleware. Note: this class relies on Rack::Cookies being used higher up in the chain.

Constant Summary collapse

LOCAL_SESSION_KEY =
"rack.session".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, configuration, directory = nil) {|env| ... } ⇒ Middleware

Make a new global session middleware.

The optional block here controls an alternate ticket retrieval method. If no ticket is stored in the cookie jar, this function is called. If it returns a non-nil value, that value is the ticket.

Parameters:

  • configuration (Configuration)
  • optional (String, Directory)

    directory the directory class name (DEPRECATED) or an actual instance of Directory

Yields:

  • if a block is provided, yields to the block to fetch session data from request state

Yield Parameters:

  • env (Hash)

    Rack request environment is passed as a yield parameter



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/global_session/rack.rb', line 50

def initialize(app, configuration, directory=nil, &block)
  @app = app

  # Initialize shared configuration
  # @deprecated require Configuration object in v4
  if configuration.instance_of?(String)
    @configuration = Configuration.new(configuration, ENV['RACK_ENV'] || 'development')
  else
    @configuration = configuration
  end

  klass = nil
  begin
    # v0.9.0 - v3.0.4: class name is the value of the 'directory' key
    klass_name = @configuration['directory']

    case klass_name
    when Hash
      # v3.0.5 and beyond: class name is in 'class' subkey
      klass_name = klass_name['class']
    when NilClass
      # the eternal default, if the class name is not provided
      klass_name = 'GlobalSession::Directory'
    end

    if klass_name.is_a?(String)
      # for apps
      klass = klass_name.to_const
    else
      # for specs that need to directly inject a class/object
      klass = klass_name
    end
  rescue Exception => e
    raise GlobalSession::ConfigurationError,
          "Invalid/unknown directory class name: #{klass_name.inspect}"
  end

  # Initialize the directory
  # @deprecated require Directory object in v4
  if klass.is_a?(Class)
    @directory = klass.new(@configuration, directory)
  elsif klass.is_a?(Directory)
    @directory = directory
  else
    raise GlobalSession::ConfigurationError,
          "Unsupported value for 'directory': expected Class or Directory, got #{klass.inspect}"
  end

  # Initialize the keystore
  @keystore = Keystore.new(@configuration)

  @cookie_retrieval = block
  @cookie_name      = @configuration['cookie']['name']
end

Instance Attribute Details

#configurationGlobalSession::Configuration



33
34
35
# File 'lib/global_session/rack.rb', line 33

def configuration
  @configuration
end

#directoryGlobalSession::Directory



36
37
38
# File 'lib/global_session/rack.rb', line 36

def directory
  @directory
end

Instance Method Details

#call(env) ⇒ Array

Rack request chain. Sets up the global session ticket from the environment and passes it up the chain.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (Array)

    valid Rack response tuple e.g. [200, ‘hello world’]



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/global_session/rack.rb', line 110

def call(env)
  env['rack.cookies'] = {} unless env['rack.cookies']

  begin
    err = nil
    read_authorization_header(env) || read_cookie(env) || create_session(env)
  rescue Exception => read_err
    err = read_err

    # Catch "double whammy" errors
    begin
      env['global_session'] = @directory.create_session
    rescue Exception => create_err
      err = create_err
    end

    handle_error('reading session cookie', env, err)
  end

  tuple = nil

  begin
    tuple = @app.call(env)
  rescue Exception => read_err
    handle_error('processing request', env, read_err)
    return tuple
  else
    renew_cookie(env)
    update_cookie(env)
    return tuple
  end
end

Determine the domain name for which we should set the cookie. Uses the domain specified in the configuration if one is found; otherwise, uses the SERVER_NAME from the request but strips off the first component if the domain name contains more than two components.

Parameters:

  • env (Hash)

    Rack request environment



314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/global_session/rack.rb', line 314

def cookie_domain(env)
  if @configuration['cookie'].has_key?('domain')
    # Use the explicitly provided domain name
    domain = @configuration['cookie']['domain']
  else
    # Use the server name, but strip off the most specific component
    parts  = env['SERVER_NAME'].split('.')
    parts  = parts[1..-1] if parts.length > 2
    domain = parts.join('.')
  end

  domain
end

#create_session(env) ⇒ true

Ensure that the Rack environment contains a global session object; create a session if necessary.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (true)

    always returns true



191
192
193
194
195
# File 'lib/global_session/rack.rb', line 191

def create_session(env)
  env['global_session'] ||= @directory.create_session

  true
end

#handle_error(activity, env, e) ⇒ true

Handle exceptions that occur during app invocation. This will either save the error in the Rack environment or raise it, depending on the type of error. The error may also be logged.

Parameters:

  • activity (String)

    name of activity during which the error happened

  • env (Hash)

    Rack request environment

  • e (Exception)

    error that happened

Returns:

  • (true)

    always returns true



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/global_session/rack.rb', line 274

def handle_error(activity, env, e)
  if env['rack.logger']
    msg = "#{e.class} while #{activity}: #{e}"
    msg += " #{e.backtrace}" unless e.is_a?(ExpiredSession)
    env['rack.logger'].error(msg)
  end

  if e.is_a?(ClientError) || e.is_a?(SecurityError)
    env['global_session.error'] = e
    wipe_cookie(env)
  elsif e.is_a? ConfigurationError
    env['global_session.error'] = e
  else
    # Don't intercept errors unless they're GlobalSession-related
    raise e
  end

  true
end

#perform_invalidation_callbacks(env, old_session, new_session) ⇒ true

Perform callbacks to directory and/or local session informing them that this session has been invalidated.

Parameters:

Returns:

  • (true)

    always returns true



301
302
303
304
305
306
307
# File 'lib/global_session/rack.rb', line 301

def perform_invalidation_callbacks(env, old_session, new_session)
  if (local_session = env[LOCAL_SESSION_KEY]) && local_session.respond_to?(:rename!)
    local_session.rename!(old_session, new_session)
  end

  true
end

#read_authorization_header(env) ⇒ Boolean

Read a global session from the HTTP Authorization header, if present. If an authorization header was found, also disable global session cookie update and renewal by setting the corresponding keys of the Rack environment.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (Boolean)

    true if the environment was populated, false otherwise



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/global_session/rack.rb', line 149

def read_authorization_header(env)
  if env.has_key? 'X-HTTP_AUTHORIZATION'
    # RFC2617 style (preferred by OAuth 2.0 spec)
    header_data = env['X-HTTP_AUTHORIZATION'].to_s.split
  elsif env.has_key? 'HTTP_AUTHORIZATION'
    # Fallback style (generally when no load balancer is present, e.g. dev/test)
    header_data = env['HTTP_AUTHORIZATION'].to_s.split
  else
    header_data = nil
  end

  if header_data && header_data.size == 2 && header_data.first.downcase == 'bearer'
    env['global_session.req.renew']  = false
    env['global_session.req.update'] = false
    env['global_session']            = @directory.load_session(header_data.last)
    true
  else
    false
  end
end

Read a global session from HTTP cookies, if present.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (Boolean)

    true if the environment was populated, false otherwise



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/global_session/rack.rb', line 174

def read_cookie(env)
  if @cookie_retrieval && (cookie = @cookie_retrieval.call(env))
    env['global_session'] = @directory.load_session(cookie)
    true
  elsif env['rack.cookies'].has_key?(@cookie_name)
    env['global_session'] = @directory.load_session(env['rack.cookies'][@cookie_name])
    true
  else
    false
  end
end

Renew the session ticket.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (true)

    always returns true



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/global_session/rack.rb', line 201

def renew_cookie(env)
  return unless @configuration['authority']
  return if env['global_session.req.renew'] == false

  if (renew = @configuration['renew']) && env['global_session'] &&
    env['global_session'].expired_at < Time.at(Time.now.utc + 60 * renew.to_i)
    env['global_session'].renew!
  end

  true
end

Update the cookie jar with the revised ticket.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (true)

    always returns true



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/global_session/rack.rb', line 217

def update_cookie(env)
  return true unless @configuration['authority']
  return true if env['global_session.req.update'] == false

  session = env['global_session']

  if session
    unless session.valid?
      old_session = session
      session     = @directory.create_session
      perform_invalidation_callbacks(env, old_session, session)
      env['global_session'] = session
    end

    value   = session.to_s
    expires = @configuration['ephemeral'] ? nil : session.expired_at
    unless env['rack.cookies'][@cookie_name] == value
      env['rack.cookies'][@cookie_name] =
        {:value    => value,
         :domain   => cookie_domain(env),
         :expires  => expires,
         :httponly => true}
    end
  else
    # write an empty cookie
    wipe_cookie(env)
  end

  true
rescue Exception => e
  wipe_cookie(env)
  raise e
end

Delete the global session cookie from the cookie jar.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (true)

    always returns true



255
256
257
258
259
260
261
262
263
264
# File 'lib/global_session/rack.rb', line 255

def wipe_cookie(env)
  return unless @configuration['authority']
  return if env['global_session.req.update'] == false

  env['rack.cookies'][@cookie_name] = {:value   => nil,
                                       :domain  => cookie_domain(env),
                                       :expires => Time.at(0)}

  true
end