3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
14 our $VERSION = "0.02";
17 __PACKAGE__->mk_accessors(qw/_sessionid _session _session_delete_reason _flash _flash_stale_keys/);
25 $c->check_session_plugin_requirements;
31 sub check_session_plugin_requirements {
34 unless ( $c->isa("Catalyst::Plugin::Session::State")
35 && $c->isa("Catalyst::Plugin::Session::Store") )
38 ( "The Session plugin requires both Session::State "
39 . "and Session::Store plugins to be used as well." );
42 Catalyst::Exception->throw($err);
49 my $cfg = ( $c->config->{session} ||= {} );
57 $c->NEXT::setup_session();
66 $c->NEXT::finalize(@_);
72 if ( my $session_data = $c->_session ) {
74 # all sessions are extended at the end of the request
76 @{ $session_data }{qw/__updated __expires/} =
77 ( $now, $c->config->{session}{expires} + $now );
79 $c->store_session_data( "session:" . $c->sessionid, $session_data );
86 if ( my $flash_data = $c->_flash ) {
87 delete @{ $flash_data }{ @{ $c->_flash_stale_keys || [] } };
88 $c->store_session_data( "flash:" . $c->sessionid, $flash_data );
95 if ( my $sid = $c->_sessionid ) {
96 no warnings 'uninitialized'; # ne __address
98 my $session_data = $c->_session || $c->_session( $c->get_session_data( "session:$sid" ) );
99 if ( !$session_data or $session_data->{__expires} < time ) {
102 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
103 $c->delete_session("session expired");
105 elsif ($c->config->{session}{verify_address}
106 && $session_data->{__address} ne $c->request->address )
109 "Deleting session $sid due to address mismatch ("
110 . $session_data->{__address} . " != "
111 . $c->request->address . ")",
113 $c->delete_session("address mismatch");
116 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
119 $c->_expire_ession_keys;
121 return $session_data;
130 if ( my $sid = $c->_sessionid ) {
131 if ( my $flash_data = $c->_flash || $c->_flash( $c->get_session_data("flash:$sid") ) ) {
132 $c->_flash_stale_keys([ keys %$flash_data ]);
140 sub _expire_ession_keys {
141 my ( $c, $data ) = @_;
145 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
146 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
147 delete $c->_session->{$key};
148 delete $expiry->{$key};
153 my ( $c, $msg ) = @_;
155 # delete the session data
156 my $sid = $c->_sessionid || return;
157 $c->delete_session_data( "session:$sid" );
159 # reset the values in the context object
161 $c->_sessionid(undef);
162 $c->_session_delete_reason($msg);
165 sub session_delete_reason {
168 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
170 $c->_session_delete_reason( @_ );
177 if ( $c->validate_session_id( my $sid = shift ) ) {
178 $c->_sessionid( $sid );
179 return unless defined wantarray;
181 my $err = "Tried to set invalid session ID '$sid'";
182 $c->log->error( $err );
183 Catalyst::Exception->throw( $err );
187 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
189 return $c->_sessionid;
192 sub validate_session_id {
193 my ( $c, $sid ) = @_;
195 $sid =~ /^[a-f\d]+$/i;
201 $c->_session || $c->_load_session || do {
202 my $sid = $c->generate_session_id;
205 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
207 $c->initialize_session_data;
213 $c->_flash || $c->_load_flash || $c->_flash( {} );
216 sub session_expire_key {
217 my ( $c, %keys ) = @_;
220 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
223 sub initialize_session_data {
228 return $c->_session({
231 __expires => $now + $c->config->{session}{expires},
234 $c->config->{session}{verify_address}
235 ? ( __address => $c->request->address )
241 sub generate_session_id {
244 my $digest = $c->_find_digest();
245 $digest->add( $c->session_hash_seed() );
246 return $digest->hexdigest;
251 sub session_hash_seed {
254 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
259 sub _find_digest () {
261 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
263 my $obj = Digest->new($alg);
269 or Catalyst::Exception->throw(
270 "Could not find a suitable Digest module. Please install "
271 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
274 return Digest->new($usable);
281 $c->NEXT::dump_these(),
284 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
297 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
298 storage and client side state required to maintain session data.
302 # To get sessions to "just work", all you need to do is use these plugins:
306 Session::Store::FastMmap
307 Session::State::Cookie
310 # you can replace Store::FastMmap with Store::File - both have sensible
311 # default configurations (see their docs for details)
313 # more complicated backends are available for other scenarios (DBI storage,
317 # after you've loaded the plugins you can save session data
318 # For example, if you are writing a shopping cart, it could be implemented
321 sub add_item : Local {
322 my ( $self, $c ) = @_;
324 my $item_id = $c->req->param("item");
326 # $c->session is a hash ref, a bit like $c->stash
327 # the difference is that it' preserved across requests
329 push @{ $c->session->{items} }, $item_id;
331 $c->forward("MyView");
334 sub display_items : Local {
335 my ( $self, $c ) = @_;
337 # values in $c->session are restored
338 $c->stash->{items_to_display} =
339 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
341 $c->forward("MyView");
346 The Session plugin is the base of two related parts of functionality required
347 for session management in web applications.
349 The first part, the State, is getting the browser to repeat back a session key,
350 so that the web application can identify the client and logically string
351 several requests together into a session.
353 The second part, the Store, deals with the actual storage of information about
354 the client. This data is stored so that the it may be revived for every request
355 made by the same client.
357 This plugin links the two pieces together.
359 =head1 RECCOMENDED BACKENDS
363 =item Session::State::Cookie
365 The only really sane way to do state is using cookies.
367 =item Session::Store::File
369 A portable backend, based on Cache::File.
371 =item Session::Store::FastMmap
373 A fast and flexible backend, based on Cache::FastMmap.
383 An accessor for the session ID value.
387 Returns a hash reference that might contain unserialized values from previous
388 requests in the same session, and whose modified value will be saved for future
391 This method will automatically create a new session and session ID if none
396 This is like Ruby on Rails' flash data structure. Think of it as a stash that
397 lasts a single redirect, not only a forward.
400 my ( $self, $c ) = @_;
402 $c->flash->{beans} = 10;
403 $c->response->redirect( $c->uri_for("foo") );
407 my ( $self, $c ) = @_;
409 my $value = $c->flash->{beans};
413 $c->response->redirect( $c->uri_for("bar") );
417 my ( $self, $c ) = @_;
419 if ( exists $c->flash->{beans} ) { # false
424 =item session_delete_reason
426 This accessor contains a string with the reason a session was deleted. Possible
441 =item session_expire_key $key, $ttl
443 Mark a key to expire at a certain time (only useful when shorter than the
444 expiry time for the whole session).
448 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
452 $c->session_expire_key( __user => 3600 );
454 Will make the session data survive, but the user will still be logged out after
457 Note that these values are not auto extended.
461 =item INTERNAL METHODS
467 This method is extended to also make calls to
468 C<check_session_plugin_requirements> and C<setup_session>.
470 =item check_session_plugin_requirements
472 This method ensures that a State and a Store plugin are also in use by the
477 This method populates C<< $c->config->{session} >> with the default values
478 listed in L</CONFIGURATION>.
482 This methoid is extended, and will restore session data and check it for
483 validity if a session id is defined. It assumes that the State plugin will
484 populate the C<sessionid> key beforehand.
488 This method is extended and will extend the expiry time, as well as persist the
489 session data if a session exists.
491 =item delete_session REASON
493 This method is used to invalidate a session. It takes an optional parameter
494 which will be saved in C<session_delete_reason> if provided.
496 =item initialize_session_data
498 This method will initialize the internal structure of the session, and is
499 called by the C<session> method if appropriate.
501 =item generate_session_id
503 This method will return a string that can be used as a session ID. It is
504 supposed to be a reasonably random string with enough bits to prevent
505 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
506 MD5 or SHA-256, depending on the availibility of these modules.
508 =item session_hash_seed
510 This method is actually rather internal to generate_session_id, but should be
511 overridable in case you want to provide more random data.
513 Currently it returns a concatenated string which contains:
515 =item validate_session_id SID
517 Make sure a session ID is of the right format.
519 This currently ensures that the session ID string is any amount of case
520 insensitive hexadecimal characters.
534 One value from C<rand>.
538 The stringified value of a newly allocated hash reference
542 The stringified value of the Catalyst context object
546 In the hopes that those combined values are entropic enough for most uses. If
547 this is not the case you can replace C<session_hash_seed> with e.g.
549 sub session_hash_seed {
550 open my $fh, "<", "/dev/random";
551 read $fh, my $bytes, 20;
556 Or even more directly, replace C<generate_session_id>:
558 sub generate_session_id {
559 open my $fh, "<", "/dev/random";
560 read $fh, my $bytes, 20;
562 return unpack("H*", $bytes);
565 Also have a look at L<Crypt::Random> and the various openssl bindings - these
566 modules provide APIs for cryptographically secure random data.
570 See L<Catalyst/dump_these> - ammends the session data structure to the list of
571 dumped objects if session ID is defined.
575 =head1 USING SESSIONS DURING PREPARE
577 The earliest point in time at which you may use the session data is after
578 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
580 State plugins must set $c->session ID before C<prepare_action>, and during
581 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
587 # don't touch $c->session yet!
589 $c->NEXT::prepare_action( @_ );
591 $c->session; # this is OK
592 $c->sessionid; # this is also OK
597 $c->config->{session} = {
601 All configuation parameters are provided in a hash reference under the
602 C<session> key in the configuration hash.
608 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
613 When true, C<<$c->request->address>> will be checked at prepare time. If it is
614 not the same as the address that initiated the session, the session is deleted.
620 The hash reference returned by C<< $c->session >> contains several keys which
621 are automatically set:
627 A timestamp whose value is the last second when the session is still valid. If
628 a session is restored, and __expires is less than the current time, the session
633 The last time a session was saved. This is the value of
634 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
638 The time when the session was first created.
642 The value of C<< $c->request->address >> at the time the session was created.
643 This value is only populated if C<verify_address> is true in the configuration.
649 =head2 Round the Robin Proxies
651 C<verify_address> could make your site inaccessible to users who are behind
652 load balanced proxies. Some ISPs may give a different IP to each request by the
653 same client due to this type of proxying. If addresses are verified these
654 users' sessions cannot persist.
656 To let these users access your site you can either disable address verification
657 as a whole, or provide a checkbox in the login dialog that tells the server
658 that it's OK for the address of the client to change. When the server sees that
659 this box is checked it should delete the C<__address> sepcial key from the
660 session hash when the hash is first created.
662 =head2 Race Conditions
664 In this day and age where cleaning detergents and dutch football (not the
665 american kind) teams roam the plains in great numbers, requests may happen
666 simultaneously. This means that there is some risk of session data being
667 overwritten, like this:
673 request a starts, request b starts, with the same session id
677 session data is loaded in request a
681 session data is loaded in request b
685 session data is changed in request a
689 request a finishes, session data is updated and written to store
693 request b finishes, session data is updated and written to store, overwriting
698 If this is a concern in your application, a soon to be developed locking
699 solution is the only safe way to go. This will have a bigger overhead.
701 For applications where any given user is only making one request at a time this
702 plugin should be safe enough.
710 =item Christian Hansen
712 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
714 =item Sebastian Riedel
718 And countless other contributers from #catalyst. Thanks guys!
720 =head1 COPYRIGHT & LICENSE
722 Copyright (c) 2005 the aforementioned authors. All rights
723 reserved. This program is free software; you can redistribute
724 it and/or modify it under the same terms as Perl itself.