1# 2# cgi/session.rb - session support for cgi scripts 3# 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 7# 8# Author: Yukihiro "Matz" Matsumoto 9# 10# Documentation: William Webber (william@williamwebber.com) 11 12require 'cgi' 13require 'tmpdir' 14 15class CGI 16 17 # == Overview 18 # 19 # This file provides the CGI::Session class, which provides session 20 # support for CGI scripts. A session is a sequence of HTTP requests 21 # and responses linked together and associated with a single client. 22 # Information associated with the session is stored 23 # on the server between requests. A session id is passed between client 24 # and server with every request and response, transparently 25 # to the user. This adds state information to the otherwise stateless 26 # HTTP request/response protocol. 27 # 28 # == Lifecycle 29 # 30 # A CGI::Session instance is created from a CGI object. By default, 31 # this CGI::Session instance will start a new session if none currently 32 # exists, or continue the current session for this client if one does 33 # exist. The +new_session+ option can be used to either always or 34 # never create a new session. See #new() for more details. 35 # 36 # #delete() deletes a session from session storage. It 37 # does not however remove the session id from the client. If the client 38 # makes another request with the same id, the effect will be to start 39 # a new session with the old session's id. 40 # 41 # == Setting and retrieving session data. 42 # 43 # The Session class associates data with a session as key-value pairs. 44 # This data can be set and retrieved by indexing the Session instance 45 # using '[]', much the same as hashes (although other hash methods 46 # are not supported). 47 # 48 # When session processing has been completed for a request, the 49 # session should be closed using the close() method. This will 50 # store the session's state to persistent storage. If you want 51 # to store the session's state to persistent storage without 52 # finishing session processing for this request, call the update() 53 # method. 54 # 55 # == Storing session state 56 # 57 # The caller can specify what form of storage to use for the session's 58 # data with the +database_manager+ option to CGI::Session::new. The 59 # following storage classes are provided as part of the standard library: 60 # 61 # CGI::Session::FileStore:: stores data as plain text in a flat file. Only 62 # works with String data. This is the default 63 # storage type. 64 # CGI::Session::MemoryStore:: stores data in an in-memory hash. The data 65 # only persists for as long as the current ruby 66 # interpreter instance does. 67 # CGI::Session::PStore:: stores data in Marshalled format. Provided by 68 # cgi/session/pstore.rb. Supports data of any type, 69 # and provides file-locking and transaction support. 70 # 71 # Custom storage types can also be created by defining a class with 72 # the following methods: 73 # 74 # new(session, options) 75 # restore # returns hash of session data. 76 # update 77 # close 78 # delete 79 # 80 # Changing storage type mid-session does not work. Note in particular 81 # that by default the FileStore and PStore session data files have the 82 # same name. If your application switches from one to the other without 83 # making sure that filenames will be different 84 # and clients still have old sessions lying around in cookies, then 85 # things will break nastily! 86 # 87 # == Maintaining the session id. 88 # 89 # Most session state is maintained on the server. However, a session 90 # id must be passed backwards and forwards between client and server 91 # to maintain a reference to this session state. 92 # 93 # The simplest way to do this is via cookies. The CGI::Session class 94 # provides transparent support for session id communication via cookies 95 # if the client has cookies enabled. 96 # 97 # If the client has cookies disabled, the session id must be included 98 # as a parameter of all requests sent by the client to the server. The 99 # CGI::Session class in conjunction with the CGI class will transparently 100 # add the session id as a hidden input field to all forms generated 101 # using the CGI#form() HTML generation method. No built-in support is 102 # provided for other mechanisms, such as URL re-writing. The caller is 103 # responsible for extracting the session id from the session_id 104 # attribute and manually encoding it in URLs and adding it as a hidden 105 # input to HTML forms created by other mechanisms. Also, session expiry 106 # is not automatically handled. 107 # 108 # == Examples of use 109 # 110 # === Setting the user's name 111 # 112 # require 'cgi' 113 # require 'cgi/session' 114 # require 'cgi/session/pstore' # provides CGI::Session::PStore 115 # 116 # cgi = CGI.new("html4") 117 # 118 # session = CGI::Session.new(cgi, 119 # 'database_manager' => CGI::Session::PStore, # use PStore 120 # 'session_key' => '_rb_sess_id', # custom session key 121 # 'session_expires' => Time.now + 30 * 60, # 30 minute timeout 122 # 'prefix' => 'pstore_sid_') # PStore option 123 # if cgi.has_key?('user_name') and cgi['user_name'] != '' 124 # # coerce to String: cgi[] returns the 125 # # string-like CGI::QueryExtension::Value 126 # session['user_name'] = cgi['user_name'].to_s 127 # elsif !session['user_name'] 128 # session['user_name'] = "guest" 129 # end 130 # session.close 131 # 132 # === Creating a new session safely 133 # 134 # require 'cgi' 135 # require 'cgi/session' 136 # 137 # cgi = CGI.new("html4") 138 # 139 # # We make sure to delete an old session if one exists, 140 # # not just to free resources, but to prevent the session 141 # # from being maliciously hijacked later on. 142 # begin 143 # session = CGI::Session.new(cgi, 'new_session' => false) 144 # session.delete 145 # rescue ArgumentError # if no old session 146 # end 147 # session = CGI::Session.new(cgi, 'new_session' => true) 148 # session.close 149 # 150 class Session 151 152 class NoSession < RuntimeError #:nodoc: 153 end 154 155 # The id of this session. 156 attr_reader :session_id, :new_session 157 158 def Session::callback(dbman) #:nodoc: 159 Proc.new{ 160 dbman[0].close unless dbman.empty? 161 } 162 end 163 164 # Create a new session id. 165 # 166 # The session id is an MD5 hash based upon the time, 167 # a random number, and a constant string. This routine 168 # is used internally for automatically generated 169 # session ids. 170 def create_new_id 171 require 'securerandom' 172 begin 173 session_id = SecureRandom.hex(16) 174 rescue NotImplementedError 175 require 'digest/md5' 176 md5 = Digest::MD5::new 177 now = Time::now 178 md5.update(now.to_s) 179 md5.update(String(now.usec)) 180 md5.update(String(rand(0))) 181 md5.update(String($$)) 182 md5.update('foobar') 183 session_id = md5.hexdigest 184 end 185 session_id 186 end 187 private :create_new_id 188 189 # Create a new CGI::Session object for +request+. 190 # 191 # +request+ is an instance of the +CGI+ class (see cgi.rb). 192 # +option+ is a hash of options for initialising this 193 # CGI::Session instance. The following options are 194 # recognised: 195 # 196 # session_key:: the parameter name used for the session id. 197 # Defaults to '_session_id'. 198 # session_id:: the session id to use. If not provided, then 199 # it is retrieved from the +session_key+ parameter 200 # of the request, or automatically generated for 201 # a new session. 202 # new_session:: if true, force creation of a new session. If not set, 203 # a new session is only created if none currently 204 # exists. If false, a new session is never created, 205 # and if none currently exists and the +session_id+ 206 # option is not set, an ArgumentError is raised. 207 # database_manager:: the name of the class providing storage facilities 208 # for session state persistence. Built-in support 209 # is provided for +FileStore+ (the default), 210 # +MemoryStore+, and +PStore+ (from 211 # cgi/session/pstore.rb). See the documentation for 212 # these classes for more details. 213 # 214 # The following options are also recognised, but only apply if the 215 # session id is stored in a cookie. 216 # 217 # session_expires:: the time the current session expires, as a 218 # +Time+ object. If not set, the session will terminate 219 # when the user's browser is closed. 220 # session_domain:: the hostname domain for which this session is valid. 221 # If not set, defaults to the hostname of the server. 222 # session_secure:: if +true+, this session will only work over HTTPS. 223 # session_path:: the path for which this session applies. Defaults 224 # to the directory of the CGI script. 225 # 226 # +option+ is also passed on to the session storage class initializer; see 227 # the documentation for each session storage class for the options 228 # they support. 229 # 230 # The retrieved or created session is automatically added to +request+ 231 # as a cookie, and also to its +output_hidden+ table, which is used 232 # to add hidden input elements to forms. 233 # 234 # *WARNING* the +output_hidden+ 235 # fields are surrounded by a <fieldset> tag in HTML 4 generation, which 236 # is _not_ invisible on many browsers; you may wish to disable the 237 # use of fieldsets with code similar to the following 238 # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805) 239 # 240 # cgi = CGI.new("html4") 241 # class << cgi 242 # undef_method :fieldset 243 # end 244 # 245 def initialize(request, option={}) 246 @new_session = false 247 session_key = option['session_key'] || '_session_id' 248 session_id = option['session_id'] 249 unless session_id 250 if option['new_session'] 251 session_id = create_new_id 252 @new_session = true 253 end 254 end 255 unless session_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) 259 end 260 unless session_id 261 session_id, = request.cookies[session_key] 262 end 263 unless session_id 264 unless option.fetch('new_session', true) 265 raise ArgumentError, "session_key `%s' should be supplied"%session_key 266 end 267 session_id = create_new_id 268 @new_session = true 269 end 270 end 271 @session_id = session_id 272 dbman = option['database_manager'] || FileStore 273 begin 274 @dbman = dbman::new(self, option) 275 rescue NoSession 276 unless option.fetch('new_session', true) 277 raise ArgumentError, "invalid session_id `%s'"%session_id 278 end 279 session_id = @session_id = create_new_id unless session_id 280 @new_session=true 281 retry 282 end 283 request.instance_eval do 284 @output_hidden = {session_key => session_id} unless option['no_hidden'] 285 @output_cookies = [ 286 Cookie::new("name" => session_key, 287 "value" => session_id, 288 "expires" => option['session_expires'], 289 "domain" => option['session_domain'], 290 "secure" => option['session_secure'], 291 "path" => 292 if option['session_path'] 293 option['session_path'] 294 elsif ENV["SCRIPT_NAME"] 295 File::dirname(ENV["SCRIPT_NAME"]) 296 else 297 "" 298 end) 299 ] unless option['no_cookies'] 300 end 301 @dbprot = [@dbman] 302 ObjectSpace::define_finalizer(self, Session::callback(@dbprot)) 303 end 304 305 # Retrieve the session data for key +key+. 306 def [](key) 307 @data ||= @dbman.restore 308 @data[key] 309 end 310 311 # Set the session data for key +key+. 312 def []=(key, val) 313 @write_lock ||= true 314 @data ||= @dbman.restore 315 @data[key] = val 316 end 317 318 # Store session data on the server. For some session storage types, 319 # this is a no-op. 320 def update 321 @dbman.update 322 end 323 324 # Store session data on the server and close the session storage. 325 # For some session storage types, this is a no-op. 326 def close 327 @dbman.close 328 @dbprot.clear 329 end 330 331 # Delete the session from storage. Also closes the storage. 332 # 333 # Note that the session's data is _not_ automatically deleted 334 # upon the session expiring. 335 def delete 336 @dbman.delete 337 @dbprot.clear 338 end 339 340 # File-based session storage class. 341 # 342 # Implements session storage as a flat file of 'key=value' values. 343 # This storage type only works directly with String values; the 344 # user is responsible for converting other types to Strings when 345 # storing and from Strings when retrieving. 346 class FileStore 347 # Create a new FileStore instance. 348 # 349 # This constructor is used internally by CGI::Session. The 350 # user does not generally need to call it directly. 351 # 352 # +session+ is the session for which this instance is being 353 # created. The session id must only contain alphanumeric 354 # characters; automatically generated session ids observe 355 # this requirement. 356 # 357 # +option+ is a hash of options for the initializer. The 358 # following options are recognised: 359 # 360 # tmpdir:: the directory to use for storing the FileStore 361 # file. Defaults to Dir::tmpdir (generally "/tmp" 362 # on Unix systems). 363 # prefix:: the prefix to add to the session id when generating 364 # the filename for this session's FileStore file. 365 # Defaults to "cgi_sid_". 366 # suffix:: the prefix to add to the session id when generating 367 # the filename for this session's FileStore file. 368 # Defaults to the empty string. 369 # 370 # This session's FileStore file will be created if it does 371 # not exist, or opened if it does. 372 def initialize(session, option={}) 373 dir = option['tmpdir'] || Dir::tmpdir 374 prefix = option['prefix'] || 'cgi_sid_' 375 suffix = option['suffix'] || '' 376 id = session.session_id 377 require 'digest/md5' 378 md5 = Digest::MD5.hexdigest(id)[0,16] 379 @path = dir+"/"+prefix+md5+suffix 380 if File::exist? @path 381 @hash = nil 382 else 383 unless session.new_session 384 raise CGI::Session::NoSession, "uninitialized session" 385 end 386 @hash = {} 387 end 388 end 389 390 # Restore session state from the session's FileStore file. 391 # 392 # Returns the session state as a hash. 393 def restore 394 unless @hash 395 @hash = {} 396 begin 397 lockf = File.open(@path+".lock", "r") 398 lockf.flock File::LOCK_SH 399 f = File.open(@path, 'r') 400 for line in f 401 line.chomp! 402 k, v = line.split('=',2) 403 @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v)) 404 end 405 ensure 406 f.close unless f.nil? 407 lockf.close if lockf 408 end 409 end 410 @hash 411 end 412 413 # Save session state to the session's FileStore file. 414 def update 415 return unless @hash 416 begin 417 lockf = File.open(@path+".lock", File::CREAT|File::RDWR, 0600) 418 lockf.flock File::LOCK_EX 419 f = File.open(@path+".new", File::CREAT|File::TRUNC|File::WRONLY, 0600) 420 for k,v in @hash 421 f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(Marshal.dump(v))) 422 end 423 f.close 424 File.rename @path+".new", @path 425 ensure 426 f.close if f and !f.closed? 427 lockf.close if lockf 428 end 429 end 430 431 # Update and close the session's FileStore file. 432 def close 433 update 434 end 435 436 # Close and delete the session's FileStore file. 437 def delete 438 File::unlink @path+".lock" rescue nil 439 File::unlink @path+".new" rescue nil 440 File::unlink @path rescue Errno::ENOENT 441 end 442 end 443 444 # In-memory session storage class. 445 # 446 # Implements session storage as a global in-memory hash. Session 447 # data will only persist for as long as the ruby interpreter 448 # instance does. 449 class MemoryStore 450 GLOBAL_HASH_TABLE = {} #:nodoc: 451 452 # Create a new MemoryStore instance. 453 # 454 # +session+ is the session this instance is associated with. 455 # +option+ is a list of initialisation options. None are 456 # currently recognised. 457 def initialize(session, option=nil) 458 @session_id = session.session_id 459 unless GLOBAL_HASH_TABLE.key?(@session_id) 460 unless session.new_session 461 raise CGI::Session::NoSession, "uninitialized session" 462 end 463 GLOBAL_HASH_TABLE[@session_id] = {} 464 end 465 end 466 467 # Restore session state. 468 # 469 # Returns session data as a hash. 470 def restore 471 GLOBAL_HASH_TABLE[@session_id] 472 end 473 474 # Update session state. 475 # 476 # A no-op. 477 def update 478 # don't need to update; hash is shared 479 end 480 481 # Close session storage. 482 # 483 # A no-op. 484 def close 485 # don't need to close 486 end 487 488 # Delete the session state. 489 def delete 490 GLOBAL_HASH_TABLE.delete(@session_id) 491 end 492 end 493 494 # Dummy session storage class. 495 # 496 # Implements session storage place holder. No actual storage 497 # will be done. 498 class NullStore 499 # Create a new NullStore instance. 500 # 501 # +session+ is the session this instance is associated with. 502 # +option+ is a list of initialisation options. None are 503 # currently recognised. 504 def initialize(session, option=nil) 505 end 506 507 # Restore (empty) session state. 508 def restore 509 {} 510 end 511 512 # Update session state. 513 # 514 # A no-op. 515 def update 516 end 517 518 # Close session storage. 519 # 520 # A no-op. 521 def close 522 end 523 524 # Delete the session state. 525 # 526 # A no-op. 527 def delete 528 end 529 end 530 end 531end 532