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

NUMERIC_HOST =
/^[0-9.]+$/.freeze
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 disk-directory in which keys live (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



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
104
105
106
107
# File 'lib/global_session/rack.rb', line 56

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 object
  if directory.is_a?(Directory)
    # In v4-style initialization, the directory is always passed in
    @directory = directory
  elsif klass.is_a?(Class)
    # @deprecated v3-style initialization where the config file names the directory class
    @directory = klass.new(@configuration, directory)
  else
    raise GlobalSession::ConfigurationError,
          "Cannot determine directory class/instance; method parameter is a #{directory.class.name} and configuration parameter is #{klass.class.name}"
  end

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

Instance Attribute Details

#configurationGlobalSession::Configuration



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

def configuration
  @configuration
end

#directoryGlobalSession::Directory



39
40
41
# File 'lib/global_session/rack.rb', line 39

def directory
  @directory
end

#keystoreGlobalSession::Keystore



42
43
44
# File 'lib/global_session/rack.rb', line 42

def keystore
  @keystore
end

Instance Method Details

#call(env) ⇒ Array

Rack request chain. Parses a global session from the request if present; makes a new session if absent; populates env with the session object and calls through to the next middleware.

On return, auto-renews the session if appropriate and writes a new session cookie if anything in the session has changed.

When reading session cookies or authorization headers, this middleware URL-decodes cookie/token values before passing them into the gem’s other logic. Some user agents and proxies “helpfully” URL-encode cookies which we need to undo in order to prevent subtle signature failures due to Base64 decoding issues resulting from “=” being URL-encoded.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (Array)

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



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/global_session/rack.rb', line 124

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



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/global_session/rack.rb', line 334

def cookie_domain(env)
  name = env['HTTP_X_FORWARDED_HOST'] || env['SERVER_NAME']

  if @configuration['cookie'].has_key?('domain')
    # Use the explicitly provided domain name
    domain = @configuration['cookie']['domain']
  elsif name =~ NUMERIC_HOST
    # Don't set a domain if the browser requested an IP-based host
    domain = nil
  else
    # Guess an appropriate domain for the cookie. Strip one level of
    # subdomain; leave SLDs unmolested; omit domain entirely for
    # one-component domains (e.g. localhost).
    parts  = name.split('.')
    case parts.length
    when 0..1
      domain = nil
    when 2
      domain = parts.join('.')
    else
      domain = parts[1..-1].join('.')
    end
  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



206
207
208
209
210
# File 'lib/global_session/rack.rb', line 206

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



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/global_session/rack.rb', line 294

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



321
322
323
324
325
326
327
# File 'lib/global_session/rack.rb', line 321

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/global_session/rack.rb', line 163

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(CGI.unescape(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



188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/global_session/rack.rb', line 188

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

Renew the session ticket.

Parameters:

  • env (Hash)

    Rack request environment

Returns:

  • (true)

    always returns true



216
217
218
219
220
221
222
223
224
225
226
# File 'lib/global_session/rack.rb', line 216

def renew_cookie(env)
  return true unless @directory.local_authority_name
  return true 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



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/global_session/rack.rb', line 232

def update_cookie(env)
  return true unless @directory.local_authority_name
  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
      secure = (env['HTTP_X_FORWARDED_PROTO'] == 'https') ||
               (env['rack.url_scheme'] == 'https')
      env['rack.cookies'][@cookie_name] =
        {
          :value    => value,
          :domain   => cookie_domain(env),
          :expires  => expires,
          :httponly => true,
          :secure   => secure,
        }
    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



275
276
277
278
279
280
281
282
283
284
# File 'lib/global_session/rack.rb', line 275

def wipe_cookie(env)
  return true unless @directory.local_authority_name
  return true if env['global_session.req.update'] == false

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

  true
end