3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
13 use Object::Signature ();
15 our $VERSION = "0.03";
18 __PACKAGE__->mk_accessors(
24 _session_delete_reason
36 $c->check_session_plugin_requirements;
42 sub check_session_plugin_requirements {
45 unless ( $c->isa("Catalyst::Plugin::Session::State")
46 && $c->isa("Catalyst::Plugin::Session::Store") )
49 ( "The Session plugin requires both Session::State "
50 . "and Session::Store plugins to be used as well." );
53 Catalyst::Exception->throw($err);
60 my $cfg = ( $c->config->{session} ||= {} );
68 $c->NEXT::setup_session();
74 if ( $c->config->{session}{flash_to_stash}
76 and my $flash_data = $c->flash )
78 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
81 $c->NEXT::prepare_action(@_);
90 $c->NEXT::finalize(@_);
96 if ( my $sid = $c->_sessionid ) {
97 if ( my $session_data = $c->_session ) {
99 # all sessions are extended at the end of the request
102 # the ref is a workaround for FastMmap:
103 # FastMmap can't store values which aren't refs
104 # this yields errors and other great suckage
105 $c->store_session_data( "expires:$sid" =>
106 ( { expires => $c->config->{session}{expires} + $now } ) );
108 no warnings 'uninitialized';
109 if ( Object::Signature::signature($session_data) ne
110 $c->_session_data_sig )
112 $session_data->{__updated} = $now;
113 $c->store_session_data( "session:$sid" => $session_data );
122 if ( my $sid = $c->_sessionid ) {
123 my $flash_data = $c->_flash || {};
125 delete @{$flash_data}{ @{ $c->_flash_stale_keys || [] } };
127 if (%$flash_data) { # damn 'my' declarations
128 $c->store_session_data( "flash:$sid", $flash_data );
131 $c->delete_session_data("flash:$sid");
139 if ( my $sid = $c->_sessionid ) {
140 no warnings 'uninitialized'; # ne __address
142 # see above for explanation of the workaround for FastMmap problem
143 my $session_expires =
144 ( $c->get_session_data("expires:$sid") || { expires => 0 } )
147 if ( $session_expires < time ) {
150 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
151 $c->delete_session("session expired");
155 my $session_data = $c->get_session_data("session:$sid");
156 $c->_session($session_data);
158 if ( $c->config->{session}{verify_address}
159 && $session_data->{__address} ne $c->request->address )
162 "Deleting session $sid due to address mismatch ("
163 . $session_data->{__address} . " != "
164 . $c->request->address . ")",
166 $c->delete_session("address mismatch");
170 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
171 $c->_session_data_sig( Object::Signature::signature($session_data) );
172 $c->_expire_session_keys;
174 return $session_data;
183 if ( my $sid = $c->_sessionid ) {
184 if ( my $flash_data = $c->_flash
185 || $c->_flash( $c->get_session_data("flash:$sid") ) )
187 $c->_flash_stale_keys( [ keys %$flash_data ] );
195 sub _expire_session_keys {
196 my ( $c, $data ) = @_;
200 my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
201 foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
202 delete $c->_session->{$key};
203 delete $expiry->{$key};
208 my ( $c, $msg ) = @_;
210 # delete the session data
211 my $sid = $c->_sessionid || return;
212 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
214 # reset the values in the context object
216 $c->_sessionid(undef);
217 $c->_session_delete_reason($msg);
220 sub session_delete_reason {
224 if ( $c->_sessionid && !$c->_session ); # must verify session data
226 $c->_session_delete_reason(@_);
233 if ( $c->validate_session_id( my $sid = shift ) ) {
234 $c->_sessionid($sid);
235 return unless defined wantarray;
238 my $err = "Tried to set invalid session ID '$sid'";
239 $c->log->error($err);
240 Catalyst::Exception->throw($err);
245 if ( $c->_sessionid && !$c->_session ); # must verify session data
247 return $c->_sessionid;
250 sub validate_session_id {
251 my ( $c, $sid ) = @_;
253 $sid and $sid =~ /^[a-f\d]+$/i;
259 $c->_session || $c->_load_session || do {
260 $c->create_session_id;
262 $c->initialize_session_data;
268 $c->_flash || $c->_load_flash || do {
269 $c->create_session_id;
274 sub session_expire_key {
275 my ( $c, %keys ) = @_;
278 @{ $c->session->{__expire_keys} }{ keys %keys } =
279 map { $now + $_ } values %keys;
282 sub initialize_session_data {
293 $c->config->{session}{verify_address}
294 ? ( __address => $c->request->address )
301 sub generate_session_id {
304 my $digest = $c->_find_digest();
305 $digest->add( $c->session_hash_seed() );
306 return $digest->hexdigest;
309 sub create_session_id {
312 if ( !$c->_sessionid ) {
313 my $sid = $c->generate_session_id;
315 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
323 sub session_hash_seed {
326 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
331 sub _find_digest () {
333 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
334 if ( eval { Digest->new($alg) } ) {
339 Catalyst::Exception->throw(
340 "Could not find a suitable Digest module. Please install "
341 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
345 return Digest->new($usable);
352 $c->NEXT::dump_these(),
355 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
368 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
369 storage and client side state required to maintain session data.
373 # To get sessions to "just work", all you need to do is use these plugins:
377 Session::Store::FastMmap
378 Session::State::Cookie
381 # you can replace Store::FastMmap with Store::File - both have sensible
382 # default configurations (see their docs for details)
384 # more complicated backends are available for other scenarios (DBI storage,
388 # after you've loaded the plugins you can save session data
389 # For example, if you are writing a shopping cart, it could be implemented
392 sub add_item : Local {
393 my ( $self, $c ) = @_;
395 my $item_id = $c->req->param("item");
397 # $c->session is a hash ref, a bit like $c->stash
398 # the difference is that it' preserved across requests
400 push @{ $c->session->{items} }, $item_id;
402 $c->forward("MyView");
405 sub display_items : Local {
406 my ( $self, $c ) = @_;
408 # values in $c->session are restored
409 $c->stash->{items_to_display} =
410 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
412 $c->forward("MyView");
417 The Session plugin is the base of two related parts of functionality required
418 for session management in web applications.
420 The first part, the State, is getting the browser to repeat back a session key,
421 so that the web application can identify the client and logically string
422 several requests together into a session.
424 The second part, the Store, deals with the actual storage of information about
425 the client. This data is stored so that the it may be revived for every request
426 made by the same client.
428 This plugin links the two pieces together.
430 =head1 RECCOMENDED BACKENDS
434 =item Session::State::Cookie
436 The only really sane way to do state is using cookies.
438 =item Session::Store::File
440 A portable backend, based on Cache::File.
442 =item Session::Store::FastMmap
444 A fast and flexible backend, based on Cache::FastMmap.
454 An accessor for the session ID value.
458 Returns a hash reference that might contain unserialized values from previous
459 requests in the same session, and whose modified value will be saved for future
462 This method will automatically create a new session and session ID if none
467 This is like Ruby on Rails' flash data structure. Think of it as a stash that
468 lasts a single redirect, not only a forward.
471 my ( $self, $c ) = @_;
473 $c->flash->{beans} = 10;
474 $c->response->redirect( $c->uri_for("foo") );
478 my ( $self, $c ) = @_;
480 my $value = $c->flash->{beans};
484 $c->response->redirect( $c->uri_for("bar") );
488 my ( $self, $c ) = @_;
490 if ( exists $c->flash->{beans} ) { # false
495 =item session_delete_reason
497 This accessor contains a string with the reason a session was deleted. Possible
512 =item session_expire_key $key, $ttl
514 Mark a key to expire at a certain time (only useful when shorter than the
515 expiry time for the whole session).
519 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
523 $c->session_expire_key( __user => 3600 );
525 Will make the session data survive, but the user will still be logged out after
528 Note that these values are not auto extended.
532 =head1 INTERNAL METHODS
538 This method is extended to also make calls to
539 C<check_session_plugin_requirements> and C<setup_session>.
541 =item check_session_plugin_requirements
543 This method ensures that a State and a Store plugin are also in use by the
548 This method populates C<< $c->config->{session} >> with the default values
549 listed in L</CONFIGURATION>.
553 This methoid is extended.
555 It's only effect is if the (off by default) C<flash_to_stash> configuration
556 parameter is on - then it will copy the contents of the flash to the stash at
561 This method is extended and will extend the expiry time, as well as persist the
562 session data if a session exists.
564 =item delete_session REASON
566 This method is used to invalidate a session. It takes an optional parameter
567 which will be saved in C<session_delete_reason> if provided.
569 =item initialize_session_data
571 This method will initialize the internal structure of the session, and is
572 called by the C<session> method if appropriate.
574 =item create_session_id
576 Creates a new session id using C<generate_session_id> if there is no session ID
579 =item generate_session_id
581 This method will return a string that can be used as a session ID. It is
582 supposed to be a reasonably random string with enough bits to prevent
583 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
584 MD5 or SHA-256, depending on the availibility of these modules.
586 =item session_hash_seed
588 This method is actually rather internal to generate_session_id, but should be
589 overridable in case you want to provide more random data.
591 Currently it returns a concatenated string which contains:
593 =item validate_session_id SID
595 Make sure a session ID is of the right format.
597 This currently ensures that the session ID string is any amount of case
598 insensitive hexadecimal characters.
612 One value from C<rand>.
616 The stringified value of a newly allocated hash reference
620 The stringified value of the Catalyst context object
624 In the hopes that those combined values are entropic enough for most uses. If
625 this is not the case you can replace C<session_hash_seed> with e.g.
627 sub session_hash_seed {
628 open my $fh, "<", "/dev/random";
629 read $fh, my $bytes, 20;
634 Or even more directly, replace C<generate_session_id>:
636 sub generate_session_id {
637 open my $fh, "<", "/dev/random";
638 read $fh, my $bytes, 20;
640 return unpack("H*", $bytes);
643 Also have a look at L<Crypt::Random> and the various openssl bindings - these
644 modules provide APIs for cryptographically secure random data.
648 See L<Catalyst/dump_these> - ammends the session data structure to the list of
649 dumped objects if session ID is defined.
653 =head1 USING SESSIONS DURING PREPARE
655 The earliest point in time at which you may use the session data is after
656 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
658 State plugins must set $c->session ID before C<prepare_action>, and during
659 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
665 # don't touch $c->session yet!
667 $c->NEXT::prepare_action( @_ );
669 $c->session; # this is OK
670 $c->sessionid; # this is also OK
675 $c->config->{session} = {
679 All configuation parameters are provided in a hash reference under the
680 C<session> key in the configuration hash.
686 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
691 When true, C<<$c->request->address>> will be checked at prepare time. If it is
692 not the same as the address that initiated the session, the session is deleted.
696 This option makes it easier to have actions behave the same whether they were
697 forwarded to or redirected to. On prepare time it copies the contents of
698 C<flash> (if any) to the stash.
704 The hash reference returned by C<< $c->session >> contains several keys which
705 are automatically set:
711 This key no longer exists. This data is now saved elsewhere.
715 The last time a session was saved to the store.
719 The time when the session was first created.
723 The value of C<< $c->request->address >> at the time the session was created.
724 This value is only populated if C<verify_address> is true in the configuration.
730 =head2 Round the Robin Proxies
732 C<verify_address> could make your site inaccessible to users who are behind
733 load balanced proxies. Some ISPs may give a different IP to each request by the
734 same client due to this type of proxying. If addresses are verified these
735 users' sessions cannot persist.
737 To let these users access your site you can either disable address verification
738 as a whole, or provide a checkbox in the login dialog that tells the server
739 that it's OK for the address of the client to change. When the server sees that
740 this box is checked it should delete the C<__address> sepcial key from the
741 session hash when the hash is first created.
743 =head2 Race Conditions
745 In this day and age where cleaning detergents and dutch football (not the
746 american kind) teams roam the plains in great numbers, requests may happen
747 simultaneously. This means that there is some risk of session data being
748 overwritten, like this:
754 request a starts, request b starts, with the same session id
758 session data is loaded in request a
762 session data is loaded in request b
766 session data is changed in request a
770 request a finishes, session data is updated and written to store
774 request b finishes, session data is updated and written to store, overwriting
779 If this is a concern in your application, a soon to be developed locking
780 solution is the only safe way to go. This will have a bigger overhead.
782 For applications where any given user is only making one request at a time this
783 plugin should be safe enough.
791 =item Christian Hansen
793 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
795 =item Sebastian Riedel
799 And countless other contributers from #catalyst. Thanks guys!
801 =head1 COPYRIGHT & LICENSE
803 Copyright (c) 2005 the aforementioned authors. All rights
804 reserved. This program is free software; you can redistribute
805 it and/or modify it under the same terms as Perl itself.