2
# cgi/session.rb - session support for cgi scripts
4
# Copyright (C) 2001 Yukihiro "Matz" Matsumoto
5
# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
6
# Copyright (C) 2000 Information-technology Promotion Agency, Japan
8
# Author: Yukihiro "Matz" Matsumoto
10
# Documentation: William Webber (william@williamwebber.com)
14
# This file provides the +CGI::Session+ class, which provides session
15
# support for CGI scripts. A session is a sequence of HTTP requests
16
# and responses linked together and associated with a single client.
17
# Information associated with the session is stored
18
# on the server between requests. A session id is passed between client
19
# and server with every request and response, transparently
20
# to the user. This adds state information to the otherwise stateless
21
# HTTP request/response protocol.
23
# See the documentation to the +CGI::Session+ class for more details
24
# and examples of usage. See cgi.rb for the +CGI+ class itself.
31
# Class representing an HTTP session. See documentation for the file
32
# cgi/session.rb for an introduction to HTTP sessions.
36
# A CGI::Session instance is created from a CGI object. By default,
37
# this CGI::Session instance will start a new session if none currently
38
# exists, or continue the current session for this client if one does
39
# exist. The +new_session+ option can be used to either always or
40
# never create a new session. See #new() for more details.
42
# #delete() deletes a session from session storage. It
43
# does not however remove the session id from the client. If the client
44
# makes another request with the same id, the effect will be to start
45
# a new session with the old session's id.
47
# == Setting and retrieving session data.
49
# The Session class associates data with a session as key-value pairs.
50
# This data can be set and retrieved by indexing the Session instance
51
# using '[]', much the same as hashes (although other hash methods
54
# When session processing has been completed for a request, the
55
# session should be closed using the close() method. This will
56
# store the session's state to persistent storage. If you want
57
# to store the session's state to persistent storage without
58
# finishing session processing for this request, call the update()
61
# == Storing session state
63
# The caller can specify what form of storage to use for the session's
64
# data with the +database_manager+ option to CGI::Session::new. The
65
# following storage classes are provided as part of the standard library:
67
# CGI::Session::FileStore:: stores data as plain text in a flat file. Only
68
# works with String data. This is the default
70
# CGI::Session::MemoryStore:: stores data in an in-memory hash. The data
71
# only persists for as long as the current ruby
72
# interpreter instance does.
73
# CGI::Session::PStore:: stores data in Marshalled format. Provided by
74
# cgi/session/pstore.rb. Supports data of any type,
75
# and provides file-locking and transaction support.
77
# Custom storage types can also be created by defining a class with
78
# the following methods:
80
# new(session, options)
81
# restore # returns hash of session data.
86
# Changing storage type mid-session does not work. Note in particular
87
# that by default the FileStore and PStore session data files have the
88
# same name. If your application switches from one to the other without
89
# making sure that filenames will be different
90
# and clients still have old sessions lying around in cookies, then
91
# things will break nastily!
93
# == Maintaining the session id.
95
# Most session state is maintained on the server. However, a session
96
# id must be passed backwards and forwards between client and server
97
# to maintain a reference to this session state.
99
# The simplest way to do this is via cookies. The CGI::Session class
100
# provides transparent support for session id communication via cookies
101
# if the client has cookies enabled.
103
# If the client has cookies disabled, the session id must be included
104
# as a parameter of all requests sent by the client to the server. The
105
# CGI::Session class in conjunction with the CGI class will transparently
106
# add the session id as a hidden input field to all forms generated
107
# using the CGI#form() HTML generation method. No built-in support is
108
# provided for other mechanisms, such as URL re-writing. The caller is
109
# responsible for extracting the session id from the session_id
110
# attribute and manually encoding it in URLs and adding it as a hidden
111
# input to HTML forms created by other mechanisms. Also, session expiry
112
# is not automatically handled.
116
# === Setting the user's name
119
# require 'cgi/session'
120
# require 'cgi/session/pstore' # provides CGI::Session::PStore
122
# cgi = CGI.new("html4")
124
# session = CGI::Session.new(cgi,
125
# 'database_manager' => CGI::Session::PStore, # use PStore
126
# 'session_key' => '_rb_sess_id', # custom session key
127
# 'session_expires' => Time.now + 30 * 60, # 30 minute timeout
128
# 'prefix' => 'pstore_sid_') # PStore option
129
# if cgi.has_key?('user_name') and cgi['user_name'] != ''
130
# # coerce to String: cgi[] returns the
131
# # string-like CGI::QueryExtension::Value
132
# session['user_name'] = cgi['user_name'].to_s
133
# elsif !session['user_name']
134
# session['user_name'] = "guest"
138
# === Creating a new session safely
141
# require 'cgi/session'
143
# cgi = CGI.new("html4")
145
# # We make sure to delete an old session if one exists,
146
# # not just to free resources, but to prevent the session
147
# # from being maliciously hijacked later on.
149
# session = CGI::Session.new(cgi, 'new_session' => false)
151
# rescue ArgumentError # if no old session
153
# session = CGI::Session.new(cgi, 'new_session' => true)
158
class NoSession < RuntimeError #:nodoc:
161
# The id of this session.
162
attr_reader :session_id, :new_session
164
def Session::callback(dbman) #:nodoc:
166
dbman[0].close unless dbman.empty?
170
# Create a new session id.
172
# The session id is an MD5 hash based upon the time,
173
# a random number, and a constant string. This routine
174
# is used internally for automatically generated
178
md5 = Digest::MD5::new
181
md5.update(String(now.usec))
182
md5.update(String(rand(0)))
183
md5.update(String($$))
188
private :create_new_id
190
# Create a new CGI::Session object for +request+.
192
# +request+ is an instance of the +CGI+ class (see cgi.rb).
193
# +option+ is a hash of options for initialising this
194
# CGI::Session instance. The following options are
197
# session_key:: the parameter name used for the session id.
198
# Defaults to '_session_id'.
199
# session_id:: the session id to use. If not provided, then
200
# it is retrieved from the +session_key+ parameter
201
# of the request, or automatically generated for
203
# new_session:: if true, force creation of a new session. If not set,
204
# a new session is only created if none currently
205
# exists. If false, a new session is never created,
206
# and if none currently exists and the +session_id+
207
# option is not set, an ArgumentError is raised.
208
# database_manager:: the name of the class providing storage facilities
209
# for session state persistence. Built-in support
210
# is provided for +FileStore+ (the default),
211
# +MemoryStore+, and +PStore+ (from
212
# cgi/session/pstore.rb). See the documentation for
213
# these classes for more details.
215
# The following options are also recognised, but only apply if the
216
# session id is stored in a cookie.
218
# session_expires:: the time the current session expires, as a
219
# +Time+ object. If not set, the session will terminate
220
# when the user's browser is closed.
221
# session_domain:: the hostname domain for which this session is valid.
222
# If not set, defaults to the hostname of the server.
223
# session_secure:: if +true+, this session will only work over HTTPS.
224
# session_path:: the path for which this session applies. Defaults
225
# to the directory of the CGI script.
227
# +option+ is also passed on to the session storage class initialiser; see
228
# the documentation for each session storage class for the options
231
# The retrieved or created session is automatically added to +request+
232
# as a cookie, and also to its +output_hidden+ table, which is used
233
# to add hidden input elements to forms.
235
# *WARNING* the +output_hidden+
236
# fields are surrounded by a <fieldset> tag in HTML 4 generation, which
237
# is _not_ invisible on many browsers; you may wish to disable the
238
# use of fieldsets with code similar to the following
239
# (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805)
241
# cgi = CGI.new("html4")
243
# undef_method :fieldset
246
def initialize(request, option={})
248
session_key = option['session_key'] || '_session_id'
249
session_id = option['session_id']
251
if option['new_session']
252
session_id = create_new_id
256
if request.key?(session_key)
257
session_id = request[session_key]
258
session_id = session_id.read if session_id.respond_to?(:read)
261
session_id, = request.cookies[session_key]
264
unless option.fetch('new_session', true)
265
raise ArgumentError, "session_key `%s' should be supplied"%session_key
267
session_id = create_new_id
270
@session_id = session_id
271
dbman = option['database_manager'] || FileStore
273
@dbman = dbman::new(self, option)
275
unless option.fetch('new_session', true)
276
raise ArgumentError, "invalid session_id `%s'"%session_id
278
session_id = @session_id = create_new_id
281
request.instance_eval do
282
@output_hidden = {session_key => session_id} unless option['no_hidden']
284
Cookie::new("name" => session_key,
285
"value" => session_id,
286
"expires" => option['session_expires'],
287
"domain" => option['session_domain'],
288
"secure" => option['session_secure'],
289
"path" => if option['session_path'] then
290
option['session_path']
291
elsif ENV["SCRIPT_NAME"] then
292
File::dirname(ENV["SCRIPT_NAME"])
296
] unless option['no_cookies']
299
ObjectSpace::define_finalizer(self, Session::callback(@dbprot))
302
# Retrieve the session data for key +key+.
304
@data ||= @dbman.restore
308
# Set the session date for key +key+.
311
@data ||= @dbman.restore
315
# Store session data on the server. For some session storage types,
321
# Store session data on the server and close the session storage.
322
# For some session storage types, this is a no-op.
328
# Delete the session from storage. Also closes the storage.
330
# Note that the session's data is _not_ automatically deleted
331
# upon the session expiring.
337
# File-based session storage class.
339
# Implements session storage as a flat file of 'key=value' values.
340
# This storage type only works directly with String values; the
341
# user is responsible for converting other types to Strings when
342
# storing and from Strings when retrieving.
344
# Create a new FileStore instance.
346
# This constructor is used internally by CGI::Session. The
347
# user does not generally need to call it directly.
349
# +session+ is the session for which this instance is being
350
# created. The session id must only contain alphanumeric
351
# characters; automatically generated session ids observe
354
# +option+ is a hash of options for the initialiser. The
355
# following options are recognised:
357
# tmpdir:: the directory to use for storing the FileStore
358
# file. Defaults to Dir::tmpdir (generally "/tmp"
360
# prefix:: the prefix to add to the session id when generating
361
# the filename for this session's FileStore file.
362
# Defaults to the empty string.
363
# suffix:: the prefix to add to the session id when generating
364
# the filename for this session's FileStore file.
365
# Defaults to the empty string.
367
# This session's FileStore file will be created if it does
368
# not exist, or opened if it does.
369
def initialize(session, option={})
370
dir = option['tmpdir'] || Dir::tmpdir
371
prefix = option['prefix'] || ''
372
suffix = option['suffix'] || ''
373
id = session.session_id
375
md5 = Digest::MD5.hexdigest(id)[0,16]
376
@path = dir+"/"+prefix+md5+suffix
377
if File::exist? @path
380
unless session.new_session
381
raise CGI::Session::NoSession, "uninitialized session"
387
# Restore session state from the session's FileStore file.
389
# Returns the session state as a hash.
394
f = File.open(@path, 'r')
395
f.flock File::LOCK_SH
398
k, v = line.split('=',2)
399
@hash[CGI::unescape(k)] = CGI::unescape(v)
402
f.close unless f.nil?
408
# Save session state to the session's FileStore file.
412
f = File.open(@path, File::CREAT|File::TRUNC|File::RDWR, 0600)
413
f.flock File::LOCK_EX
415
f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v))
418
f.close unless f.nil?
422
# Update and close the session's FileStore file.
427
# Close and delete the session's FileStore file.
434
# In-memory session storage class.
436
# Implements session storage as a global in-memory hash. Session
437
# data will only persist for as long as the ruby interpreter
440
GLOBAL_HASH_TABLE = {} #:nodoc:
442
# Create a new MemoryStore instance.
444
# +session+ is the session this instance is associated with.
445
# +option+ is a list of initialisation options. None are
446
# currently recognised.
447
def initialize(session, option=nil)
448
@session_id = session.session_id
449
unless GLOBAL_HASH_TABLE.key?(@session_id)
450
unless session.new_session
451
raise CGI::Session::NoSession, "uninitialized session"
453
GLOBAL_HASH_TABLE[@session_id] = {}
457
# Restore session state.
459
# Returns session data as a hash.
461
GLOBAL_HASH_TABLE[@session_id]
464
# Update session state.
468
# don't need to update; hash is shared
471
# Close session storage.
475
# don't need to close
478
# Delete the session state.
480
GLOBAL_HASH_TABLE.delete(@session_id)